destogod/test/sheet.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

52 lines
2.2 KiB
JavaScript

/** Column-mapping checks for spreadsheet import. */
import { readCsv, mapRows } from '../src/codec/sheet.mjs';
let fails = 0;
function check(label, got, want) {
const ok = JSON.stringify(got) === JSON.stringify(want);
if (!ok) { fails++; console.log(`${label}\n got ${JSON.stringify(got)}\n want ${JSON.stringify(want)}`); }
else console.log(`${label}`);
}
// The template shipped with these buses: a "Line" row number AND a "Line Name".
// The name must win over the number — this was a real bug.
check('shipped template layout',
mapRows(readCsv(
'Company Name:,Yutong Coaches\n' +
'\n' +
'Line,Line Name,Description,Content (Display)\n' +
'1,SCHOOL BUS,morning,SCHOOL BUS\n' +
'2,CARMEL,college run,CARMEL COLLEGE\n'
)),
{ company: 'Yutong Coaches', destinations: [
{ name: 'SCHOOL BUS', text: 'SCHOOL BUS' },
{ name: 'CARMEL', text: 'CARMEL COLLEGE' },
] });
check('only a number column plus content — do not show the driver a bare number',
mapRows(readCsv('Line,Content (Display)\n1,DEPOT\n2,RAIL BUS\n')).destinations,
[{ name: 'DEPOT', text: 'DEPOT' }, { name: 'RAIL BUS', text: 'RAIL BUS' }]);
check('no header at all',
mapRows(readCsv('SCHOOL BUS\nCHARTER\n')).destinations,
[{ name: 'SCHOOL BUS', text: 'SCHOOL BUS' }, { name: 'CHARTER', text: 'CHARTER' }]);
check('destination column naming',
mapRows(readCsv('Name,Destination\nAIRPORT,AIRPORT SHUTTLE VIA CITY\n')).destinations,
[{ name: 'AIRPORT', text: 'AIRPORT SHUTTLE VIA CITY' }]);
check('blank rows and stray whitespace ignored',
mapRows(readCsv('Line Name,Content\n\n DEPOT , DEPOT VIA YARD \n\n')).destinations,
[{ name: 'DEPOT', text: 'DEPOT VIA YARD' }]);
check('controller name is capped at the 16 characters the record holds',
mapRows(readCsv('Line Name,Content\nA VERY LONG DESTINATION NAME,SOMETHING\n')).destinations[0].name,
'A VERY LONG DEST');
check('quoted fields with commas',
mapRows(readCsv('Line Name,Content\n"CITY, VIA MALL","CITY, VIA MALL"\n')).destinations,
[{ name: 'CITY, VIA MALL', text: 'CITY, VIA MALL' }]);
console.log(fails === 0 ? '\n✅ spreadsheet mapping correct' : `\n${fails} failed`);
process.exit(fails ? 1 : 0);