destogod/test/tp5roundtrip.mjs
type-two 07977624ae DestoGod: replacement for the TP5 bus destination sign software
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>
2026-08-21 13:06:25 +10:00

21 lines
1.3 KiB
JavaScript

import { readFileSync } from 'node:fs';
import { parseTp5, buildTp5, toSimple } from '../src/codec/tp5.mjs';
const orig = readFileSync(new URL('../samples/EXPRESS.tp5', import.meta.url), 'latin1');
const doc = parseTp5(orig);
const out = buildTp5(doc);
console.log('companies:', doc.companies.map(c => `${c.name}(${c.destinations.length})`).join(', '));
console.log(out === orig ? '✅ .tp5 round-trip byte-identical' : '❌ .tp5 round-trip differs');
if (out !== orig) {
const a = orig.split('\r\n'), b = out.split('\r\n');
console.log(` lines ${a.length} vs ${b.length}`);
for (let i = 0, n = 0; i < Math.max(a.length, b.length) && n < 6; i++)
if (a[i] !== b[i]) { console.log(` L${i+1}\n orig: ${String(a[i]).slice(0,110)}\n new : ${String(b[i]).slice(0,110)}`); n++; }
}
const s = toSimple(doc);
console.log(`\nsimple view: company=${s.company} screen=${s.screen.width}x${s.screen.height}`);
for (const d of s.destinations.slice(0, 5))
console.log(` ${d.name.padEnd(18)} -> ${d.frames.map(f => `"${f.text}" [${f.useTrueType ? f.ttfName + ' ' + f.ptSize : f.font}] fx=${f.effect}`).join(' + ')}`);
const ch = s.destinations.find(d => d.frames.length > 1);
if (ch) console.log(` ${ch.name.padEnd(18)} -> ${ch.frames.map(f => `"${f.text}"`).join(' + ')} (${ch.frames.length} frames)`);