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:
- Windows 11 - the stock in-box HP DeskJet 500 driver
- Linux / macOS - Gutenprint pcl-500 at 300 dpi, through CUPS
- Android - NokoPrint, "HP DeskJet 500 (Gutenprint)" or "Generic PCL4", mono 300 dpi
- Anything else - any generic PCL 3/4 mono 300 dpi raster driver
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
- Compression mode 0 (raw) or 2 (PCL TIFF/packbits),
ESC*b0MorESC*b2M. Not 1 or 3. - 300 dpi, single-plane mono.
- Rows under 2000 bytes each,
ESC*b<n>W. - 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
- Works. Gutenprint
pcl-500at 300 dpi including therastertogutenprintCUPS filter, the Windows DeskJet 500 driver, and generic PCL4 mono 300 dpi drivers with no delta. - Safe but blank. Compression modes 1 and 3 and any other
*b#Mvalue are consumed count-based and printed blank. Vertical spacing stays right and a later*b0Mor*b2Mrecovers. Color-plane data on uppercaseESC*b<n>Vis discarded byte-exact, so if the driver sends K as the finalWplane, K still prints. - Ignored.
ESC*p#X/#Ycursor moves,ESC*t#Rresolutions other than 300,ESC&lpage format,ESC&apositioning,ESC&k,ESC(fonts and symbol sets,ESC*r#S/#T,*r#Q/*o#Q/*o#D. AnyESC*with a lowercase group letter has its parameters parsed and dropped and a finalWorVpayload consumed byte-exact. Nothing leaks. - Avoid. Color DeskJet 500C and 550C drivers. Their compact plane encoding puts binary behind a lowercase
ESC*b#v, which is indistinguishable from a parameter to any grammar parser, so the binary floods the text path as garbage glyphs and can stall the job. Use mono DeskJet 500 or generic PCL4. Do not send PCL5 or HP-GL/2 either. - One operational gotcha. If the host fires the instant it opens the port while the printer is still mid-init, the first byte can be lost. The leading
ESCdrops, its partner prints as one stray glyph, the first band misparses, and then it self-heals at the nextESC. Release the job when the printer is idle. That one is not a firmware bug, it is a race, and I would rather write it down than have somebody file it.
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.
- Nothing ports. I checked all 14 patch-site signatures byte by byte against the 150 Plus, the 180si and the 300w. Zero matches on any of them. They are separate builds of a later firmware generation. Worse for the Plus, its ring is about 2049 bytes and my EMIT walk-back maxes at 2360, so the full-band emit cannot fit at all. That is a design change, not a relocation. The algorithmic core retargets fine, but every constant has to be re-derived. The 300w is a different board entirely and I would not try.
- The 180si
ESC 204width probe is unrun. That undocumented command raises the right carriage limit atC0BDpast the default 1360 columns. Four-digit parameter, three and six digit forms break. I have not run the probe to find the real ceiling. - The original four typefaces cannot come back on this ROM. Two of them are stored where my blob lives. Variety in the self-test is done with metrics and italic instead. Full multi-typeface behavior needs a clean-room ROM that owns all four font slots, which is a separate project.
- v5i is simulator-validated with metal confirmation still pending. v5h's font-force is metal-proven and v5i is the same loader mechanism one level down, but I am not going to call it hardware-proven until I have the page in my hand. The self-test pages photographed on this project are v5h, printed off real hardware. The full-page shot in the gallery is also a good look at this cosmetic limit: five of its six sliding-ASCII diagnostic rows render in the same font, which is exactly what v5i's pointer-table rewrite is meant to undo.
- The 150 Plus is the one I actually grew up with. Which is a good joke at my expense, given the first bullet in this list. The machine that gave me six years of two-thirds pages is the one machine DiKon cannot run on. It is on the list.
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.
- Stock Diconix 150 ROM (you supply) - 32,768 bytes -
a258fa60ac1611b885fd32aad2dd2b58 - DiKon v5i, 32 KB for a 27C256 - 32,768 bytes -
03a386608973e1c686d49d1ef0439410 - DiKon v5i, 64 KB doubled for a W27C512 - 65,536 bytes -
2d0d67cc221731332a82db06d4a3da2d - d150_dikon_v5i.ips - 2,096 bytes -
7ba6978f61e21508ec75cc995737f133
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
TheRND2