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>
32 lines
943 B
Python
32 lines
943 B
Python
import sys, struct
|
|
from capstone import *
|
|
import pefile
|
|
|
|
EXE = sys.argv[3] if len(sys.argv)>3 else '/Users/jing/Documents/yutong/yutongapp/TP5(En).exe'
|
|
d = open(EXE,'rb').read()
|
|
BASE = 0x400000
|
|
pe = pefile.PE(EXE)
|
|
|
|
# import name map
|
|
imp = {}
|
|
for e in pe.DIRECTORY_ENTRY_IMPORT:
|
|
for i in e.imports:
|
|
imp[i.address] = (e.dll.decode(), i.name.decode() if i.name else 'ord%d'%i.ordinal)
|
|
|
|
md = Cs(CS_ARCH_X86, CS_MODE_32)
|
|
md.detail = True
|
|
|
|
start = int(sys.argv[1],16)
|
|
n = int(sys.argv[2],16) if len(sys.argv)>2 else 0x200
|
|
|
|
off = start - BASE
|
|
for ins in md.disasm(d[off:off+n], start):
|
|
s = "%08x %-24s %s %s" % (ins.address, ins.bytes.hex(), ins.mnemonic, ins.op_str)
|
|
# annotate indirect calls
|
|
if ins.mnemonic in ('call','jmp') and 'dword ptr [0x' in ins.op_str:
|
|
try:
|
|
a = int(ins.op_str.split('[')[1].split(']')[0],16)
|
|
if a in imp: s += ' ; %s!%s' % imp[a]
|
|
except: pass
|
|
print(s)
|