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>
28 lines
1.5 KiB
JavaScript
28 lines
1.5 KiB
JavaScript
/** Compare the structure of a TP5-made file against one we authored. */
|
|
import { readFileSync } from 'node:fs';
|
|
|
|
function dump(buf, label) {
|
|
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
const ptrs = [];
|
|
for (let i = 0x200; ; i += 4) { const v = dv.getUint32(i, true); if (v === 0xffffffff) break; ptrs.push(v); }
|
|
const hx = (a, b) => Buffer.from(buf.subarray(a, b)).toString('hex').replace(/(..)/g, '$1 ').trim();
|
|
console.log(`=== ${label} (${buf.length} bytes, ${ptrs.length} destinations)`);
|
|
console.log(` 0x30..0x48 : ${hx(0x30, 0x48)}`);
|
|
console.log(` 0x80..0xa0 : ${hx(0x80, 0xa0)}`);
|
|
console.log(` 0xa0..0xb0 : ${hx(0xa0, 0xb0)}`);
|
|
for (let r = 0; r < 2 && r < ptrs.length; r++) {
|
|
const q = ptrs[r];
|
|
console.log(` rec${r} name : ${JSON.stringify(String.fromCharCode(...buf.subarray(q, q + 0x10)))}`);
|
|
console.log(` rec${r} line : ${JSON.stringify(String.fromCharCode(...buf.subarray(q + 0x10, q + 0x20)))}`);
|
|
console.log(` rec${r} third : ${JSON.stringify(String.fromCharCode(...buf.subarray(q + 0x20, q + 0x30)))}`);
|
|
console.log(` rec${r} tail : ${hx(q + 0x30, q + 0x60)}`);
|
|
console.log(` rec${r} tail2 : ${hx(q + 0x60, q + 0x80)}`);
|
|
}
|
|
}
|
|
|
|
const a = new Uint8Array(readFileSync(new URL('../samples/EXPRESS 1.td5', import.meta.url)));
|
|
const b = new Uint8Array(readFileSync(new URL('./authored.td5', import.meta.url)));
|
|
dump(a, 'TP5 original — works on the controller');
|
|
console.log();
|
|
dump(b, 'DestoGod, built from scratch');
|