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>
60 lines
2.1 KiB
JavaScript
60 lines
2.1 KiB
JavaScript
/**
|
|
* Inline every module into one self-contained HTML file, so the whole tool is a
|
|
* single document that opens straight from the desktop with no install, no
|
|
* server and no internet.
|
|
*/
|
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
const root = join(here, '..');
|
|
|
|
// Dependency order, leaves first.
|
|
const MODULES = [
|
|
'src/fonts.js',
|
|
'src/codec/bitfont.mjs',
|
|
'src/codec/td5.mjs',
|
|
'src/codec/tp5.mjs',
|
|
'src/codec/sheet.mjs',
|
|
'app/main.mjs',
|
|
];
|
|
|
|
function strip(src, name) {
|
|
const out = src
|
|
// drop the import lines; every symbol ends up in one shared scope
|
|
.replace(/^\s*import\s+[^;]*?from\s*['"][^'"]+['"];?\s*$/gm, '')
|
|
// `export const X` -> `const X`, `export function f` -> `function f`
|
|
.replace(/^export\s+(const|let|var|function|class|async)\b/gm, '$1')
|
|
// bare re-export statements, if any ever appear
|
|
.replace(/^export\s*\{[^}]*\};?\s*$/gm, '');
|
|
if (/^\s*(import|export)\b/m.test(out)) {
|
|
throw new Error(`${name}: an import/export slipped through the bundler`);
|
|
}
|
|
return `\n// ===== ${name} =====\n${out.trim()}\n`;
|
|
}
|
|
|
|
const code = MODULES.map((m) => strip(readFileSync(join(root, m), 'utf8'), m)).join('\n');
|
|
|
|
const html = readFileSync(join(root, 'app/index.html'), 'utf8');
|
|
if (!html.includes('<script type="module" src="./main.mjs"></script>')) {
|
|
throw new Error('index.html no longer has the expected script tag');
|
|
}
|
|
|
|
const banner = `<!--
|
|
DestoGod — bus destination sign editor.
|
|
Single self-contained file: no install, no internet, nothing to set up.
|
|
Built ${new Date().toISOString().slice(0, 10)} from destogod/src + destogod/app.
|
|
-->
|
|
`;
|
|
|
|
const bundled = banner + html.replace(
|
|
'<script type="module" src="./main.mjs"></script>',
|
|
`<script type="module">\n${code}\n</script>`
|
|
);
|
|
|
|
mkdirSync(join(root, 'dist'), { recursive: true });
|
|
const outPath = join(root, 'dist/DestoGod.html');
|
|
writeFileSync(outPath, bundled);
|
|
console.log(`wrote dist/DestoGod.html (${(bundled.length / 1024).toFixed(0)} KB, ${MODULES.length} modules inlined)`);
|