TP5 is a 2012 Windows application supplied with the Guangzhou-Tongda LED
destination signs fitted to Yutong buses. It has no preview, so building a
destination list is guess-and-check, and it only runs on Windows.
The bus never talks to TP5 — the sign controller reads a .td5 file off an SD
card, and that is the whole interface. So this replaces the software without
touching any hardware or protocol: it just has to write byte-correct .td5.
Formats reverse-engineered from the sample files and TP5(En).exe, then verified
byte-for-byte:
.td5 the file the bus reads. Fixed-layout binary; each destination block
carries a CRC-16/ARC over block[3..len] and a rand() block id, which
together looked like one 4-byte field because RAND_MAX is 0x7fff.
.tp5 the editable project. Line-based text, UTF-16BE hex strings.
.font the sign's own bitmap fonts, each glyph row XORed with its char code.
The app is one self-contained HTML file: live LED preview at the real sign size
with real scrolling, spreadsheet/CSV import, multi-page destinations, undoable
delete, and export to both .td5 and .tp5.
Verified:
- rebuilds a real 46,080-byte TP5 export byte-for-byte with a recomputed CRC
- all 36 stored CRCs verify against the implementation
- driven through its own UI, re-exporting the real file differs in 7 bytes,
all of them the export timestamp
- running on a real bus: signs and driver's controller both correct
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
45 lines
1.3 KiB
JavaScript
45 lines
1.3 KiB
JavaScript
/** Minimal PNG writer, just enough for test previews. */
|
|
import { deflateSync } from 'node:zlib';
|
|
|
|
function crc32(buf) {
|
|
let c, crc = 0xffffffff;
|
|
for (let n = 0; n < buf.length; n++) {
|
|
c = (crc ^ buf[n]) & 0xff;
|
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
crc = c ^ (crc >>> 8);
|
|
}
|
|
return (crc ^ 0xffffffff) >>> 0;
|
|
}
|
|
|
|
function chunk(type, data) {
|
|
const len = Buffer.alloc(4);
|
|
len.writeUInt32BE(data.length);
|
|
const body = Buffer.concat([Buffer.from(type, 'latin1'), data]);
|
|
const crc = Buffer.alloc(4);
|
|
crc.writeUInt32BE(crc32(body));
|
|
return Buffer.concat([len, body, crc]);
|
|
}
|
|
|
|
/** rgb: (x,y) => [r,g,b] */
|
|
export function writePNG(width, height, rgb) {
|
|
const raw = Buffer.alloc((width * 3 + 1) * height);
|
|
let p = 0;
|
|
for (let y = 0; y < height; y++) {
|
|
raw[p++] = 0;
|
|
for (let x = 0; x < width; x++) {
|
|
const [r, g, b] = rgb(x, y);
|
|
raw[p++] = r; raw[p++] = g; raw[p++] = b;
|
|
}
|
|
}
|
|
const ihdr = Buffer.alloc(13);
|
|
ihdr.writeUInt32BE(width, 0);
|
|
ihdr.writeUInt32BE(height, 4);
|
|
ihdr[8] = 8; ihdr[9] = 2; ihdr[10] = 0; ihdr[11] = 0; ihdr[12] = 0;
|
|
return Buffer.concat([
|
|
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
chunk('IHDR', ihdr),
|
|
chunk('IDAT', deflateSync(raw)),
|
|
chunk('IEND', Buffer.alloc(0)),
|
|
]);
|
|
}
|