The Kodak Diconix 150 is a battery-powered portable thermal inkjet released in 1986 by Diconix Inc. of Dayton, Ohio. Kodak bought Diconix in 1988, which is why the machine wears a Kodak badge and the stock ROM prints "A KODAK COMPANY" on its self-test page.

The board is silkscreened UK-EP01B MAIN CONTROL PWB. On it is a NEC D78C10G at 11.0592 MHz carrying a date code of 8604EK, which is to say it was made in early 1986 and has been sitting there ever since. 32 KB of ROM in a socketed Fujitsu MBM27C256-25, 8 KB of SRAM, a Centronics port, and an HP 51604A ThinkJet head. Twelve nozzles, eight of them usable per pass, 96 dpi per pass. There is no ROM checksum anywhere on the boot path, which I verified before I touched anything, and which is the entire reason this project is possible.

It speaks a fixed Epson-style ESC text command set and nothing else. Which means nothing made this century can print to it. Windows will not offer you a driver, CUPS has no queue for it, and your phone has never heard of it.

There is a second reason I wanted this machine specifically. I grew up on one. From 1989 to about 1995 a Kodak-badged Diconix was the only printer in the house, and every graphic it ever printed for me came out at about two thirds size: three quarters tall, five eighths wide. That turns out to be a grid mismatch nobody had written down. The hardware lays dots at 96 or 192 dpi horizontal by 96 dpi vertical, per HP's own service manual for the 2225 ThinkJet engine. Epson's FX graphics standard assumes 60, 120 or 240 dpi across and 72 down. Nothing in the chain rescales, so vertical always lands at 72/96 and double-density horizontal lands at 120/192. Two different fractions on two axes, which is why the aspect looked squeezed as well as small. I spent six years assuming that was just how printers were. Fixing it thirty years later by writing the printer new firmware is the shape this project took.

What I did about that

I did not write it a driver. I wrote it new firmware.

DiKon is a from-scratch PCL3 raster interpreter in uPD78C10 assembly, patched into the stock Kodak ROM in space reclaimed from the resident fonts. The interpreter runs on the printer's own 8-bit CPU. 300 dpi PCL raster goes in the parallel port and gets decoded and re-interleaved on-chip to the head's native 192 dpi vertical by 1152-dot full-carriage grid.

The whole point of putting it in there instead of on a PC is that the host then needs nothing at all. Not a converter, not a spooler, not a print processor, not an install:

Always 300 x 300 dpi and mono. 150 dpi prints at half scale, legible but wrong size.

The Android row is the one I care about. A phone in your pocket printing to a printer built the year Top Gun came out, over a Centronics cable, with no app written for it by anybody.

Bone-stock hardware. No board modification, no piggyback, no daughtercard. Reflash the 27C256 or drop in a doubled W27C512 and it runs.

How it works

The stock ESC dispatch table at 0x196A gets its ESC* entry repointed at my parser at 0x2800. ESC& and ESC( are pointed at a swallow routine, because they are free on the plain 150 and harmless to consume. Six patched bytes in the table. Every other ESC handler stays stock, so the printer's native text path is untouched. The rest of DiKon is one blob living at 0x2800 to 0x2F28, in a ROM window that was proven unreferenced rather than merely looking empty.

The pipeline is:

ESC*  ->  M7STAR parser  ->  DISPATCH  ->  PROCESS_ROW  ->  column buffers  ->  EMIT  ->  stock ring                                          (decode + bit)   COLSA / COLSB     (walk-back)   THR / DTHR throttle

It never buffers a page. It processes one 16-row band at a time into 8 KB of RAM.

The interleave is the part I am proudest of. The head prints 8 usable rows per pass at 96 dpi. A band is 16 output rows, built as two interleaved 8-nozzle passes: even rows into COLSA, odd rows into COLSB, woven one 1/192 inch step apart. That is where the true 192 dpi vertical comes from, and it happens on the printer's CPU instead of on a PC pre-processing a file. That was the endgame the whole project was aimed at.

EMIT is a walk-back. SCANPASS finds the last inked column group, PASSBYTES computes the exact byte count, and EMIT walks back and writes only that. A blank pass emits no ESC L at all and costs 8 bytes. It cut emitted bytes by 41 to 78 percent with pixel-identical output.

Flow control rides the stock full-flag VV:C8 rather than any BUSY path of my own. THR at 0x0609 trips it at the enqueue threshold, and the mandatory dequeue-side twin DTHR at 0x0644 clears it, because the stock C8-clear only runs when the write pointer equals the read pointer and a threshold-tripped C8 would otherwise never clear on its own. More on why I know that in the failures below.

Set these four knobs and any driver works

  1. Compression mode 0 (raw) or 2 (PCL TIFF/packbits), ESC*b0M or ESC*b2M. Not 1 or 3.
  2. 300 dpi, single-plane mono.
  3. Rows under 2000 bytes each, ESC*b<n>W.
  4. End the job with ESC*rB. That is the flush that prints the last partial band.

What matters is the raster encoding the driver emits, not the PCL level printed on the box. A "generic PCL4 mono" driver emitting mode 0/2 raster is, at the byte level this printer sees, indistinguishable from the DeskJet 500's own PCL3.

Works, blank, ignored, avoid

Mode 3, delta row, is deliberately not implemented. Gutenprint has no delta-row encoder at all, and a seedless decode over OR-accumulating column buffers would be wrong rather than merely incomplete.

The self-test page is mine now

The 150's power-on self-test lives in ROM, and DiKon rewrites it. Hold the panel button at power-on and the machine prints an RND2 Labs banner: the project name, the build ID, KODAK DICONIX 150 1986, a WAS line and a NOW line, the driver to use, and the project URL, over the stock star rows and head diagnostics.

That page is firmware I wrote, printing out of 1986 hardware, on a machine that has no business doing it. It is also the single most useful diagnostic in the project, because it proves the chip is seated, the halves match, and the text engine is intact before you send it a single byte.

What didn't work

This is the honest part, and it is longer than the part that worked. The emulator lied to me twice, and both times hardware told the truth.

v2b and v2c smeared black. Byte-perfect in the simulator, solid black on paper. The ESC L fire path runs with interrupts on, so the Centronics strobe producer kept filling the ring during EMIT and my prepend clobbered rows the engine had not read yet. That is the core tension of the whole design in one sentence: EMIT writes into the same ring the host is filling.

v3a and v3b did not throttle at all. I had assumed an un-read data latch at 0x8000 holds BUSY. It does not. Disassembly proved BUSY is asserted by the stock full-logic at VV:C8 (0x0610) plus a Port C line at 0x0272. My read-gate never asserted BUSY on real hardware, so the throttle was decorative. Lesson, and I would put it on a wall: do not invent a BUSY path, ride the stock one.

v3d and v3e wedged hard, and this is the one worth reading. I prepended a single CR to EMIT to force column-zero anchoring. On hardware that CR hit the stock CR handler at 0x1B48 into 0x1B55, which checks motion-in-flight before it checks buffer-empty. So it queued a pending print against an empty buffer, and the next ESC L aborted after its own header, and 224 raster bytes leaked into the text path and hard-wedged the machine. My stubbed simulator hid it completely. It took building an unstubbed audit rig to reproduce it instruction for instruction. One character. Do not burn v3d or v3e.

v5f bricked a printer. Constant BUSY on every interface, crash and reset loop every few seconds, garbage on the rare moments it was stable. The v5f self-test banner extended to 0x3000 through 0x3018, and that region is live resident-font glyph storage. It holds the cells for @, A and B of the stride-21 font based at 0x2D61, plus cells 240 to 242 of the stride-12 font based at 0x24B5. Those base addresses live as data inside font parameter blocks at 0x0B0F and 0x0B24, reached through a pointer table at 0x0AFF. They are invisible to an instruction-operand scan, which is exactly how I cleared that window in the first place, and my emulator rig never touched them because its jobs only ever selected the two other quality classes. A dead scan plus an incomplete test equals a bricked printer. The rule that came out of it is permanent and asserted in the build now: nothing goes at or above 0x3000.

v5g was withdrawn for six bytes. The clipped banner still overran the metal-proven text ceiling, because that ceiling is 0x2FF4 and not 0x2FFF. 0x2FF5 through 0x3000 is glyph cell 240 of the stride-12 font, virgin stock in every firmware that ever worked. Six bytes.

Then the self-test itself printed garbage. Four of the six sliding-ASCII diagnostic rows walked the glyph cells my blob is sitting on. The fix turned out to already be in Kodak's own firmware: there is a DIP-selected quality lock at 0x0ACB through 0x0AD2 that forces every font class to one of the two fonts I did not touch. v5h makes that lock unconditional with a single byte, 0x0ACE: JR -> NOP. v5i then restores the visual variety a different way, by rewriting the class-to-parameter-block pointer table at 0x0AFF so all eight classes resolve to an intact font base while their metrics still differ.

Smaller ones, for completeness. An 8-bit parameter accumulator wrapped on a 1552-byte row and leaked raw raster as text. A limit check compared against the address of the limit instead of its value and overran into the ring. EMIT only fired on a full 32-row band, so a 1263-row job stranded its last 15 rows. Gutenprint's *o2Q and *o5D fell through the parser and printed "2Q5D" as four garbage characters on every page.

What's still unknown

I would rather list these than pretend they are done.

If you own one of these machines, or a 150 Plus, or you have ever traced a uPD7810 ring buffer, I would like to hear from you.

How it ships, and why it ships that way

The assembled v5i image is about 93.7 percent unmodified Kodak code. 30,741 of 32,768 bytes are byte-identical to the original. My own code is 2,033 bytes, about 6.2 percent: five small patch sites plus the blob and the self-test text at 0x2800 to 0x2FFA.

So the repository ships IPS patches containing only my bytes, plus an applier that verifies the stock ROM's MD5 going in and both output MD5s coming out and refuses to write anything on a mismatch. There are no ROM images and no Kodak code in the tree. You bring your own dump, off a printer you own, read with any EPROM programmer.

On a 64 KB chip, verify both 32 KB halves are identical. A half-select fault runs stock-looking code with modified vectors, and the symptom is a printer that eats the job silently.

My code, the tools and the docs are MIT. The original ROM contents remain Eastman Kodak's and are not in the patch and cannot be reconstructed from it.

Repo, docs and patches: github.com/TheRND2/dikon
Project home: thernd2.com/diconix