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>
49 lines
1.3 KiB
Python
49 lines
1.3 KiB
Python
import struct
|
|
from parse import records
|
|
|
|
POLY = 0xA001
|
|
def crc16(data, init=0, poly=POLY):
|
|
c = init
|
|
for b in data:
|
|
c ^= b
|
|
for _ in range(8):
|
|
c = (c >> 1) ^ poly if c & 1 else c >> 1
|
|
return c & 0xffff
|
|
|
|
rs = records()
|
|
for r in rs:
|
|
r['chk'] = struct.unpack_from('<H', r['blk'], 1)[0]
|
|
|
|
# candidate ranges (start, end) end=None means to end of block
|
|
cands = []
|
|
for s in [0,1,3,5,7,0xb,0xf,0x10,0x11,0x14,0x16,0x20,0x116,0x117]:
|
|
cands.append((s, None))
|
|
cands += [(0,3),(3,7),(0,0x116),(3,0x116),(7,0x116),(0x116,None)]
|
|
|
|
blks = [r['blk'] for r in rs]
|
|
chks = [r['chk'] for r in rs]
|
|
|
|
best = []
|
|
for (s,e) in cands:
|
|
ok = 0
|
|
for b,c in zip(blks,chks):
|
|
seg = b[s:] if e is None else b[s:e]
|
|
for init in (0, 0xffff):
|
|
for xo in (0, 0xffff):
|
|
pass
|
|
if crc16(seg) == c: ok += 1
|
|
if ok: print('range', hex(s), e, 'matches', ok, '/', len(rs))
|
|
best.append((ok,s,e))
|
|
best.sort(reverse=True)
|
|
print('top:', best[:5])
|
|
|
|
# also try with the crc bytes zeroed
|
|
print('--- zeroed crc field ---')
|
|
for (s,e) in cands:
|
|
ok=0
|
|
for b,c in zip(blks,chks):
|
|
bb = bytearray(b); bb[1]=0; bb[2]=0
|
|
seg = bytes(bb[s:] if e is None else bb[s:e])
|
|
if crc16(seg) == c: ok+=1
|
|
if ok: print('range', hex(s), e, 'matches', ok)
|