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>
57 lines
2.2 KiB
Python
57 lines
2.2 KiB
Python
"""TP5 .td5 block header checksum - solved.
|
|
|
|
Block layout (first 0x13 bytes):
|
|
+0x00 u8 0x43 'C' block type tag
|
|
+0x01 u16 CRC-16/ARC over block[3:blockLen] <-- the "mystery field" low half
|
|
+0x03 u32 rand() (MSVCRT, 0..0x7FFF; srand(time(NULL)) once per export)
|
|
+0x07 u32 blockLen
|
|
+0x0b u8 1
|
|
+0x0c u8 1
|
|
+0x0d u16 1
|
|
+0x0f u8 0x84
|
|
+0x10 u16 0x0116 (offset of bitmap data = header size)
|
|
The 4 bytes read as one u32-LE at +1 are therefore CRC | (rand<<16).
|
|
"""
|
|
import struct
|
|
from parse import records
|
|
|
|
def crc16_arc(data, init=0, poly=0xA001):
|
|
"""TP5 sub_409150 with polyIndex=1 (table @0x447020 = [8480,A001,8621,E950])."""
|
|
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
|
|
|
|
def td5_block_checksum(block):
|
|
"""block: the full block bytes (length == u32 at block[7:11]). Returns u16 for block[1:3]."""
|
|
blen = struct.unpack_from('<I', block, 7)[0]
|
|
return crc16_arc(block[3:blen])
|
|
|
|
def build_header(blen, rand_val, bitmap_off=0x0116):
|
|
"""Return the first 0x13 header bytes with a placeholder CRC (fill after body is built)."""
|
|
return bytes([0x43, 0, 0]) + struct.pack('<I', rand_val) + struct.pack('<I', blen) \
|
|
+ bytes([1, 1]) + struct.pack('<H', 1) + bytes([0x84]) + struct.pack('<H', bitmap_off)
|
|
|
|
if __name__ == '__main__':
|
|
rs = records()
|
|
ok = 0
|
|
print("%-18s %6s %-9s %-9s %-6s %-6s %s" %
|
|
("record", "blen", "field(exp)", "field(calc)", "crcExp", "crcCalc", "rand"))
|
|
for r in rs:
|
|
b = r['blk']
|
|
exp_crc = struct.unpack_from('<H', b, 1)[0]
|
|
rnd = struct.unpack_from('<I', b, 3)[0]
|
|
got = td5_block_checksum(b)
|
|
field_exp = r['field']
|
|
field_calc = (got | (rnd << 16)) & 0xFFFFFFFF
|
|
good = (got == exp_crc)
|
|
ok += good
|
|
print("%-18s %6d %08x %08x %04x %04x %04x %s" %
|
|
(r['name'], r['blklen'], field_exp, field_calc, exp_crc, got, rnd,
|
|
"OK" if good else "**MISMATCH**"))
|
|
print("\n%d/%d records match" % (ok, len(rs)))
|
|
print("all rand() values <= 0x7FFF (RAND_MAX):",
|
|
all(struct.unpack_from('<I', r['blk'], 3)[0] <= 0x7FFF for r in rs))
|