Fix BSOD on DesignJet T120 / T520 for free?

Being a curious sort of guy – and having an inexplicable love for tinkering with large format printers – I couldn’t pass up an offer for a really cheap, but non functioning, DesignJet T520. Can I fix it?

It had the dreaded BSOD (Blue Screen of Death) which seems to have plagued many, many users on the internet. At least according to Google.

The remedy seems to be either to:

  1. Call HP Service and pay an absurd amount of money for a fix
  2. Order an “HP Encryption USB Key” (part number CQ890-67105)
  3. Invent some sort of way of fixing it yourself

The guy I bought it from was told by HP that they estimated the repair at around $900, but it could run further up if it was a “serious problem”. Let’s hope it’s not, so I’m not paying that much to get this fixed. So the first option is ruled out.

Second option almost seems like cheating, doesn’t it? I mean, inventive people on ebay are selling these USB keys, and they don’t look original to me. Option two is clearly an option though, as an original key is quite cheap at around $40 here in Denmark.

As the last one is by far the most time wasting and challenging, I just *had* to try it. Who knows – I might fail – and I might succeed. Either way, I’ll probably learn something along the way.

So step 1 was to dismantle the printer, to get to the logic board. This requires you to remove the lover front panel, by yanking it out (it’s held in with 6 clips), and removing 5-8 screws from the left panel. Then slide that off, and you have access to the main board. There are three screws holding that in place, so remove those too.

To remove the “HP Encryption USB Key” you probably need to remove a small screw holding it in place (accessible from the front of the main board). Then remove whatever cables needed from the upper portion of the main board (be careful with the wide LCD cable). Then carefully remove the USB key – I was able to do it without removing the board entirely.

So with the mysterious key in hand, I plugged it into a Linux machine, where it appeared as a 1.9GB FAT32 formatted drive. WTF – everywhere I look, it’s reported as a non-standard USB key. This seems to be an outright lie, getting you to choose option 1 or perhaps 2 instead.

Disk /dev/sdc: 1.9 GiB, 2003828736 bytes, 3913728 sectors
Disk model: USB DISK 2.0    
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x00000000

Device     Boot  Start     End Sectors  Size Id Type
/dev/sdc1           63   65598   65536   32M  b W95 FAT32
/dev/sdc2        65599  131134   65536   32M  b W95 FAT32
/dev/sdc3       131135 3911679 3780545  1.8G  b W95 FAT32

Further investigation (fdisk) showed this MBR partition table:

So three partitions, of which only the last one was formatted as a drive. I dd’ed all data onto my computer and started looking at it. The MBR is just an MBR partition table with no boot code at all.

Partition 1 is firmware and so is partition 2. If I was to make an updatable printer, I’d put two copies of the same firmware in the system, and have the printer boot off the first one. If that fails (firmware update aborted or other failure), I’d fall back to the secondary. This is obviously what the HP guys thought too, so there was these firmwares on each partition:

Partition 1: AXP2CN1437AR....Tue Sep 09, 2014 08:32:54AM
Partition 2: AXP2CN1325AR....Tue Jun 18, 2013 12:01:45AM

OK, so the USB drive is just an USB drive – I’ll just get another one (4GB Kingston) and dd everything back onto this one. Perhaps that would work?

…… no dice. Printer just stands there after loading a while, total black screen and beeps in disgust: “Try harder, human”.

Back at the drawing board, I tried dd’ing the secondary partition onto the primary partition, using the original HP drive. Plugged that into the printer – same result. OK, that’s really strange I thought – perhaps I didn’t dd it correctly. So I copied partition1 back onto the computer and compared it to the dump of partition2

Binary files hp-t520-fw-partition1 and x1 differ

WHAT? Whatwhatwhat … aaaah, okay, so the USB key is actually broken. I mean it works, but whatever you put on the key is not what you get back. So the onboard NAND is hosed. This explains why neither the primary firmware or secondary works.

Alright – off to HPs site, download a new firmware file, and put that on the Kingston drive? But no – not so easy. Firmware files for this printer is in the .FUL format, which stands for “fail u l0ser” (no it doesn’t), as it’s a PJL formatted file that you’re supposed to send to the printer to be able to upgrade it. So it’s not binary – well it is, but it’s a mess of mysterious blocks and whatnot, so it wasn’t going onto the USB key in the original format.

Here’s a dump of the first couple of lines:

00000000   1B 25 2D 31  32 33 34 35  .%-12345
00000008   58 40 50 4A  4C 20 43 4F  X@PJL CO
00000010   4D 4D 45 4E  54 20 28 6E  MMENT (n
00000018   75 6C 6C 29  0A 40 50 4A  ull).@PJ
00000020   4C 20 45 4E  54 45 52 20  L ENTER
00000028   4C 41 4E 47  55 41 47 45  LANGUAGE
00000030   3D 46 57 55  50 44 41 54  =FWUPDAT
00000038   45 0A 1B 45  54 68 69 73  E..EThis
00000040   20 64 65 76  69 63 65 20   device
00000048   64 6F 65 73  20 6E 6F 74  does not
00000050   20 73 75 70  70 6F 72 74   support
00000058   20 46 57 55  50 44 41 54   FWUPDAT
00000060   45 21 0D 0A  1B 2A 72 74  E!...*rt
00000068   31 36 33 38  34 73 41 1B  16384sA.
00000070   2A 62 31 36  35 34 33 59  *b16543Y
00000078   1B 2A 62 2B  30 59 1B 2A  .*b+0Y.*
00000080   62 31 36 33  38 34 56 FE  b16384V.
00000088   ED F0 0A 02  02 01 B0 00  ........
00000090   04 00 08 01  00 9E 84 25  .......%
00000098   E6 67 30 D7  F3 3B 3B DE  .g0..;;.
000000A0   5C 94 48 15  63 C6 39 77  \.H.c.9w
000000A8   01 B1 47 D4  77 28 97 8B  ..G.w(..
000000B0   0F A0 DF 92  99 38 D1 B3  .....8..
000000B8   72 93 BE F5  09 A6 E8 25  r......%
000000C0   4D 73 47 37  37 FC 1A B6  MsG77...
000000C8   D0 85 4F 3C  F8 05 76 28  ..O<..v(
000000D0   5A A1 F9 2C  2C C8 9F 98  Z..,,...
000000D8   F6 BA C8 91  40 A8 0C 5A  ....@..Z

Googling for variations of HP printer firmware, FWUPDATE etc. yielded nothign usable, but quite an interesting read on hacking other HP printers.

So a mysterious format to tinker with – and I had dumps of earlier versions of the firmware (with random corruptions to spice everything up), so perhaps patience and staring would bring a solution.

Lots of staring.

After two nights of patience, I’d reversed the format to my satisfaction and reassembled the firmware from the update. It involved reverse engineering the block format of the update, which involved plain blocks, RLE compressed blocks and 2 blocks of some sort of repeating zeroes compression format that I had to figure out. Fun times!

The code for everything is here – it’s written in Go, which is my favourite programming language of all time:

package main

import (
	"bufio"
	"errors"
	"fmt"
	"os"
	"strconv"
	"strings"

	"github.com/davecgh/go-spew/spew"
)

type stateinfo uint8

const (
	LookingForESC stateinfo = iota
	LookingForStar
	LookingForUpperCase
	Data
	DataSkip
)

type decodemode uint8

const (
	Normal decodemode = iota // b
	RLE                      // b2m
	Zeroes
)

func main() {
	debug := false
	f, err := os.Open("amperexl_pr_AXP2CN1829BR_secure_signed_rbx.ful")
	if err != nil {
		fmt.Printf("Problem opening source file: %v\n", err)
		os.Exit(1)
	}
	defer f.Close()
	s := bufio.NewReader(f)

	tf, err := os.Create("target.bin")
	if err != nil {
		fmt.Printf("Problem creating destination file: %v\n", err)
		os.Exit(1)
	}
	defer tf.Close()
	t := bufio.NewWriter(tf)
	defer t.Flush()

	pos := 0
	var blocksize, totalsize int
	var state stateinfo
	var dm decodemode
	var datasize, dataleft int
	var firstfound, problem bool
	var commandpos int
	var command string
	// var commands int
	output := make([]byte, 0, 16384)

	var problems, blocks0, blocks1, blocks2, blocks3, blocksp, bytesread int

	fmt.Println("Processing ...")

	var skipfirst int
	if !debug {
		// Header
		t.Write([]byte{0xFE, 0xED, 0xF0, 0x0D})
		skipfirst = 0x10E // This might need correction
	}

	//	var header = make([]byte, 5)
	//	var expectedheader = []byte{0xFE, 0xED, 0xF0, 0x0A, 0x02}

	// s.Read(header)
	// if !bytes.Equal(header, expectedheader) {
	// 	fmt.Printf("Wrong header detected - I got %0X, but expected %0X\n", header, expectedheader)
	// 	os.Exit(1)
	// }

mainloop:
	for {
		switch state {
		case LookingForESC:
			d, err := s.ReadByte()
			if err != nil {
				fmt.Printf("Problem reading data: %v\n", err)
				break mainloop
			}
			if d == 0x1B {
				problem = false
				command = ""
				state++
			} else if firstfound && !problem {
				fmt.Print("Not at ESC\n")
				problem = true
				problems++
			}
		case LookingForStar:
			d, err := s.ReadByte()
			if err != nil {
				fmt.Printf("Problem reading data: %v\n", err)
				break mainloop
			}
			if d == 0x2A {
				commandpos = pos - 1
				state++
				firstfound = true
			} else {
				state = LookingForESC
			}
		case LookingForUpperCase:
			d, err := s.ReadByte()
			if err != nil {
				fmt.Printf("Problem reading data: %v\n", err)
				break mainloop
			}
			command = command + string(d)
			if d >= 'A' && d <= 'Z' {
				state = LookingForESC
				switch {
				case strings.HasPrefix(command, "rt"):
					blocksize, _, err = getint(command[2:])
					if debug {
						fmt.Fprintf(t, "%08X: *%v - blocksize set to %v\n", commandpos, command, blocksize)
					}
				case command == "rC":
					if debug {
						fmt.Fprintf(t, "%08X: *%v - end of update\n", commandpos, command)
					}
					break mainloop
				case strings.HasPrefix(command, "b+1ym") || strings.HasPrefix(command, "b+2ym"):
					dm = Normal
					if debug {
						fmt.Fprintf(t, "%08X: *%v - checksum\n", commandpos, command)
					}
					datasize, command, err = getint(command[5:])
					if err != nil {
						fmt.Printf("Error parsing command, integer not found: %v\n", command)
						break mainloop
					}
					dataleft = datasize
					state = DataSkip
				case strings.HasPrefix(command, "b+"):
					dm = Normal
					if debug {
						fmt.Fprintf(t, "%08X: *%v - block+\n", commandpos, command)
					}
					blocksp++
					datasize, command, err = getint(command[2:])
					if err != nil {
						fmt.Printf("Error parsing command, integer not found: %v\n", command)
						break mainloop
					}
					if datasize > 0 {
						dataleft = datasize
						state = Data
					}
				case strings.HasPrefix(command, "b"):
					rest := command[1:]
					switch {
					case strings.HasPrefix(rest, "0m"):
						fmt.Println("Switching to normal")
						dm = Normal
						rest = rest[2:]
					case strings.HasPrefix(rest, "2m"):
						fmt.Println("Switching to RLE")
						dm = RLE
						rest = rest[2:]
					case strings.HasPrefix(rest, "3m"):
						fmt.Println("Switching to Zeroes")
						dm = Zeroes
						// dm = Normal
						rest = rest[2:]
					}
					if debug {
						fmt.Fprintf(t, "%08X: *%v - block\n", commandpos, command)
					}
					blocks1++
					datasize, rest, err = getint(rest)
					if err != nil {
						fmt.Printf("Error parsing command, integer not found: %v\n", command)
						break mainloop
					}
					dataleft = datasize
					state = Data
				default:
					fmt.Printf("%08X: *%v - UNKNOWN\n", commandpos, command)
					os.Exit(1)
				}
			}
		case DataSkip:
			if dataleft != 0 {
				_, err := s.ReadByte()
				if err != nil {
					fmt.Printf("Problem reading data: %v\n", err)
					break mainloop
				}
				dataleft--
			}
			if dataleft == 0 {
				state = LookingForESC
			}
		case Data:
			if dataleft > 0 {
				d, err := s.ReadByte()
				if err != nil {
					fmt.Printf("Problem reading data: %v\n", err)
					break mainloop
				}
				bytesread++
				dataleft--
				switch dm {
				case Zeroes:
					// First three are normal chars
					// Next five are zeroes repeated
					normals := int(d>>5) + 1
					zeroes := int(d & 0x1F)
					if zeroes == 0x1F {
						for {
							morezeroes, _ := s.ReadByte()
							zeroes += int(morezeroes)
							dataleft--
							if morezeroes != 0xFF {
								break
							}
						}
					}
					for i := 0; i < zeroes; i++ {
						output = append(output, 0)
					}
					for i := 0; i < normals; i++ {
						d, _ = s.ReadByte()
						dataleft--
						output = append(output, d)
					}
				case Normal:
					output = append(output, d)
				case RLE:
					if d > 0x80 {
						// Repeat next character 0xFF - d + 2 times
						repeat := 0xFF - int(d) + 2
						d, _ = s.ReadByte()
						dataleft--
						for i := 0; i < repeat; i++ {
							output = append(output, d)
						}
					} else {
						// Next d+1 chars are regular
						for i := 0; i <= int(d); i++ {
							n, _ := s.ReadByte()
							dataleft--
							output = append(output, n)
						}
					}
				}
			}
			if dataleft == 0 {
				state = LookingForESC
				if len(output) != 0 && len(output) != blocksize {
					fmt.Printf("Incorrect block size detected: %v\n", len(output))
				}
				if debug {
					spew.Fdump(t, output[:blocksize])
				} else {
					t.Write(output[skipfirst:blocksize])
					skipfirst = 0
				}
				totalsize += blocksize
				output = make([]byte, 0, 16384)
			}
		}
		if state == Data && strings.HasSuffix(command, "Y") {
			state = LookingForESC
			dataleft = 0
			datasize = 0
		}
		pos++
	}
	fmt.Printf("Problems: %v - Blocks: %v %v %v %v %v - total %v blocks - %v bytes\n", problems, blocks0, blocks1, blocks2, blocks3, blocksp, blocks0+blocks1+blocks2+blocks3+blocksp, bytesread)
	fmt.Printf("Total size: %v\n", totalsize)
}

var NoIntegerFound = errors.New("No integer found")

func getint(s string) (num int, rest string, err error) {
	var is string
	for len(s) > 0 && s[0] >= '0' && s[0] <= '9' {
		is = is + string(s[0])
		s = s[1:]
	}
	if is == "" {
		err = NoIntegerFound
		return
	}
	num, err = strconv.Atoi(is)
	rest = s
	return
}

The firmware update from HP has a signature at the start of the file, beginning with FE ED F0 0A …. the firmware starts from 83 50 00 00, and on the USB key it has to start with FE ED F0 0D (I see what you did there, HP!) so I’m writing the header myself and skipping the signature. You *might* need to adjust this if HP decides to change anything at a later point in time.

My resulting file was 28327670 bytes long, which I kinda-sorta validated by looking at offset 0x00000008 in the file, which contains the value 0x01AFFFF8. The file length is 16 bytes more than that, and comparing various offsets from the corrupted version looked like I got everything wrong.

Exciting, isn’t it? So I dd’ed my manually decoded firmware file to my Kingston drive to both partition1 and partition2. With a smug smile on my face I plugged it in ….. and it booted straight up!

So in conclusion, to fix this kind of problem:

  1. Get a USB 2.0 flash drive in a size ranging from 2GB to 8GB (my guess)
  2. Partition it with fdisk, using the fdisk dump from earlier in this post (notice: partion1 starts at sector 63), also I think you can make partition3 the size of the rest of the drive, but I didn’t experiment.
  3. Download latest firmware for your printer from HP (beware, there are two variants depending on the age/model of your printer) and run it through my program. There are some debug output, don’t worry about this. Check that first bytes in file is FE ED F0 0D 83 50 00 00 and that the next 4 bytes represent the total file size minus 16 bytes.
  4. Copy the firmware onto partition1 and partition2
  5. Format partition3 as FAT32

The above fixed my new scrap printer – it might not be universal, but if you know what you’re doing, hopefully this should help you get everything sorted out.

Have fun!

This entry was posted in Uncategorized. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.