evocode 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- evocode/__init__.py +13 -0
- evocode/caf.py +86 -0
- evocode/catalog.py +127 -0
- evocode/cli.py +680 -0
- evocode/coding.py +106 -0
- evocode/consent.py +32 -0
- evocode/fsc.py +274 -0
- evocode/hsfz.py +193 -0
- evocode/keys.py +137 -0
- evocode/l3.py +39 -0
- evocode/psdz.py +171 -0
- evocode/root.py +76 -0
- evocode/srec.py +98 -0
- evocode/swdl.py +423 -0
- evocode/uds.py +154 -0
- evocode-0.1.0.dist-info/METADATA +353 -0
- evocode-0.1.0.dist-info/RECORD +22 -0
- evocode-0.1.0.dist-info/WHEEL +5 -0
- evocode-0.1.0.dist-info/entry_points.txt +2 -0
- evocode-0.1.0.dist-info/licenses/LICENSE +201 -0
- evocode-0.1.0.dist-info/licenses/NOTICE +13 -0
- evocode-0.1.0.dist-info/top_level.txt +1 -0
evocode/swdl.py
ADDED
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""SWDL flash choreography — enter bootloader, authenticate L3, transfer a container, finalize.
|
|
3
|
+
|
|
4
|
+
This is the diagnostic-plane flash that coding write already relies on conceptually: same HSFZ
|
|
5
|
+
transport, same L3 key, different session (programming 0x02 instead of coding 0x41) and different
|
|
6
|
+
services (requestDownload / transferData instead of writeDID). The bootloader processes the
|
|
7
|
+
container and writes the enclosed file to the path named in the container's metadata.
|
|
8
|
+
|
|
9
|
+
Reference behaviour: ``nbt-evo-ssh-unlocker/nbt_optdebug_flash.py`` (no LICENSE file; treated as
|
|
10
|
+
behavioural reference for the wire protocol; attribution in CREDITS.md).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import time
|
|
15
|
+
from typing import Callable, Optional
|
|
16
|
+
|
|
17
|
+
from . import l3
|
|
18
|
+
from .hsfz import Hsfz, TGT_HU
|
|
19
|
+
from .uds import Uds, NRC_NAMES as _UDS_NRC
|
|
20
|
+
|
|
21
|
+
# ------------------------------------------------------------------------------- UDS / SWDL constants
|
|
22
|
+
|
|
23
|
+
SID_ROUTINE = 0x31
|
|
24
|
+
SID_RD = 0x34 # requestDownload
|
|
25
|
+
SID_TD = 0x36 # transferData
|
|
26
|
+
SID_TX = 0x37 # transferExit
|
|
27
|
+
SID_WDBI = 0x2E # writeDataByIdentifier
|
|
28
|
+
|
|
29
|
+
SA_SEED_FLASH = 0x11 # SecurityAccess requestSeed level for flash
|
|
30
|
+
SA_KEY_FLASH = 0x12 # SecurityAccess sendKey level for flash
|
|
31
|
+
|
|
32
|
+
DID_FINGERPRINT = 0xF15A
|
|
33
|
+
DID_ADS = 0xF186 # activeDiagnosticSession
|
|
34
|
+
DID_ENERGY = 0x100A
|
|
35
|
+
DID_EXTMODE = 0x100E
|
|
36
|
+
DID_FTIMING = 0x2504
|
|
37
|
+
DID_RASS = 0xF100
|
|
38
|
+
|
|
39
|
+
RID_ERASE = 0xFF00
|
|
40
|
+
RID_CHECKMEM = 0x0202
|
|
41
|
+
RID_DEPCHECK = 0xFF01
|
|
42
|
+
|
|
43
|
+
ERASE_METHOD_IE = 0x02
|
|
44
|
+
CHECK_METHOD_ICV = 0x12
|
|
45
|
+
ERASE_ACTIVATION = 0x06
|
|
46
|
+
ALFI = 0x44
|
|
47
|
+
MAX_BLOCKSIZE = 0xFFF1 # 65521
|
|
48
|
+
|
|
49
|
+
# prog-mode UDS frames
|
|
50
|
+
EXT_SESSION = bytes([0x10, 0x03])
|
|
51
|
+
PROG_SESSION = bytes([0x10, 0x02])
|
|
52
|
+
HARD_RESET = bytes([0x11, 0x01])
|
|
53
|
+
ENERGY_ON = bytes.fromhex("31010F0C03")
|
|
54
|
+
ENERGY_OFF = bytes.fromhex("31010F0C00")
|
|
55
|
+
DTC_OFF = bytes.fromhex("8502")
|
|
56
|
+
DTC_ON = bytes.fromhex("8501")
|
|
57
|
+
COMM_QUIET = bytes.fromhex("280301")
|
|
58
|
+
COMM_NORMAL = bytes.fromhex("280001")
|
|
59
|
+
EXT_SET_01 = bytes.fromhex("3101100301")
|
|
60
|
+
EXT_SET_OFF = bytes.fromhex("3101100300")
|
|
61
|
+
ACT_APP_MODE = bytes.fromhex("3101100F")
|
|
62
|
+
|
|
63
|
+
# Shared UDS names (evocode.uds) + the NRCs the flash loop specifically handles.
|
|
64
|
+
NRC_NAMES = {
|
|
65
|
+
**_UDS_NRC,
|
|
66
|
+
0x12: "subFunctionNotSupported",
|
|
67
|
+
0x21: "busyRepeatRequest",
|
|
68
|
+
0x24: "requestSequenceError",
|
|
69
|
+
0x37: "requiredTimeDelayNotExpired",
|
|
70
|
+
0x73: "wrongBlockSequenceCounter",
|
|
71
|
+
0x78: "responsePending",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def is_neg(r: bytes) -> bool:
|
|
76
|
+
return len(r) >= 1 and r[0] == 0x7F
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ------------------------------------------------------------------------------- container format
|
|
80
|
+
|
|
81
|
+
def build_container(data: bytes, target_path: str, name: str,
|
|
82
|
+
tag: str = "FIL", cpu: str = "Omap") -> bytes:
|
|
83
|
+
"""Build a SWDL no-signature file-copy container from raw file bytes.
|
|
84
|
+
|
|
85
|
+
Three records: header (0x01), metadata (0x02), file data (0x04).
|
|
86
|
+
"""
|
|
87
|
+
meta = "TargetPath=%s;Name=%s;Tag=%s;CPU=%s;" % (target_path, name, tag, cpu)
|
|
88
|
+
meta_b = meta.encode("latin-1")
|
|
89
|
+
|
|
90
|
+
def _rec(marker: int, flags1: int, flags2: int, payload: bytes) -> bytes:
|
|
91
|
+
return (bytes([marker]) + flags1.to_bytes(2, "little") +
|
|
92
|
+
len(payload).to_bytes(4, "little") + flags2.to_bytes(2, "little") + payload)
|
|
93
|
+
|
|
94
|
+
return (_rec(0x01, 1, 1, b"\x01") +
|
|
95
|
+
_rec(0x02, 2, 1, meta_b) +
|
|
96
|
+
_rec(0x04, 2, 1, data))
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def container_target(img: bytes) -> tuple[Optional[str], Optional[str], Optional[bool]]:
|
|
100
|
+
"""Parse the metadata record of a SWDL container: (full_path, name, is_live_direct)."""
|
|
101
|
+
pos, n = 0, len(img)
|
|
102
|
+
while pos + 9 <= n:
|
|
103
|
+
marker = img[pos]
|
|
104
|
+
length = int.from_bytes(img[pos + 3:pos + 7], "little")
|
|
105
|
+
if marker == 0x02:
|
|
106
|
+
meta = img[pos + 9:pos + 9 + length].decode("latin-1", "replace")
|
|
107
|
+
kv = dict(p.split("=", 1) for p in meta.split(";") if "=" in p)
|
|
108
|
+
tp = kv.get("TargetPath", "")
|
|
109
|
+
nm = kv.get("Name", "")
|
|
110
|
+
return (tp + nm, nm, bool(tp) and "/repository/istep/" not in tp)
|
|
111
|
+
pos += 9 + length
|
|
112
|
+
return None, None, None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ------------------------------------------------------------------------------- low-level helpers
|
|
116
|
+
|
|
117
|
+
def _tx(h: Hsfz, target: int, uds: bytes, label: str, log: Callable) -> Optional[bytes]:
|
|
118
|
+
try:
|
|
119
|
+
r = h.request(target, uds)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
log(" [tx] %-30s -> ERR %s" % (label, e))
|
|
122
|
+
return None
|
|
123
|
+
if is_neg(r):
|
|
124
|
+
nrc = r[2] if len(r) > 2 else 0
|
|
125
|
+
log(" [tx] %-30s -> NRC 0x%02X %s" % (label, nrc, NRC_NAMES.get(nrc, "?")))
|
|
126
|
+
else:
|
|
127
|
+
log(" [tx] %-30s -> %s" % (label, r[:12].hex()))
|
|
128
|
+
return r
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _ispos(r: Optional[bytes]) -> bool:
|
|
132
|
+
return r is not None and r[0] != 0x7F
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _val(r: Optional[bytes]) -> Optional[int]:
|
|
136
|
+
return r[-1] if _ispos(r) and r[0] == 0x62 else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _read_session(h: Hsfz, target: int, label: str, log: Callable) -> Optional[int]:
|
|
140
|
+
r = _tx(h, target, bytes([0x22, 0xF1, 0x86]), label, log)
|
|
141
|
+
return r[3] if _ispos(r) and r[0] == 0x62 and len(r) >= 4 else None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------------------- frame builders
|
|
145
|
+
|
|
146
|
+
def _fr_erase(lbn: int) -> bytes:
|
|
147
|
+
return (bytes([SID_ROUTINE, 0x01, RID_ERASE >> 8, RID_ERASE & 0xFF,
|
|
148
|
+
ERASE_METHOD_IE, ALFI]) + lbn.to_bytes(4, "big") + bytes([ERASE_ACTIVATION]))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _fr_check(lbn: int) -> bytes:
|
|
152
|
+
return (bytes([SID_ROUTINE, 0x01, RID_CHECKMEM >> 8, RID_CHECKMEM & 0xFF,
|
|
153
|
+
CHECK_METHOD_ICV, ALFI]) + lbn.to_bytes(4, "big") + bytes([0x00, 0x00]))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _fr_req_download(addr: int, size: int) -> bytes:
|
|
157
|
+
nlen = (ALFI >> 4) & 0x0F
|
|
158
|
+
nsz = ALFI & 0x0F
|
|
159
|
+
return bytes([SID_RD, 0x00, ALFI]) + addr.to_bytes(nlen, "big") + size.to_bytes(nsz, "big")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _parse_block_length(resp: Optional[bytes], default: int = 0x400) -> int:
|
|
163
|
+
if not resp or resp[0] != SID_RD + 0x40 or len(resp) < 3:
|
|
164
|
+
return default
|
|
165
|
+
nbytes = (resp[1] >> 4) & 0x0F
|
|
166
|
+
if nbytes == 0 or len(resp) < 2 + nbytes:
|
|
167
|
+
return default
|
|
168
|
+
maxblk = int.from_bytes(resp[2:2 + nbytes], "big")
|
|
169
|
+
return max(1, min(maxblk - 2, MAX_BLOCKSIZE)) if maxblk > 2 else default
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------------------------------- prog-mode entry
|
|
173
|
+
|
|
174
|
+
def enter_progmode(h: Hsfz, target: int = TGT_HU, reset_timeout: int = 50,
|
|
175
|
+
log: Callable = print) -> bool:
|
|
176
|
+
"""Enter the flash bootloader (programming session 0x02).
|
|
177
|
+
|
|
178
|
+
Returns True if the HU is in session 0x02 (bootloader). The HU self-reboots on 10 02;
|
|
179
|
+
we close/reopen the TCP connection and poll 22 F186 until it answers 0x02.
|
|
180
|
+
"""
|
|
181
|
+
log("== STEP 1: extended session + flash timing ==")
|
|
182
|
+
_tx(h, target, EXT_SESSION, "10 03 ExtendedSession", log)
|
|
183
|
+
_tx(h, target, bytes([0x22, 0x25, 0x04]), "22 2504 FlashTiming", log)
|
|
184
|
+
|
|
185
|
+
log("== STEP 2: ActivateFlashMode ==")
|
|
186
|
+
if not _ispos(_tx(h, target, ENERGY_ON, "31 01 0F0C 03 ActivateFlash", log)):
|
|
187
|
+
log(" *** HU refused ActivateFlash — abort. ***")
|
|
188
|
+
return False
|
|
189
|
+
if _val(_tx(h, target, bytes([0x22, 0x10, 0x0A]), "22 100A EnergyMode", log)) in (None, 0x00):
|
|
190
|
+
log(" *** EnergyMode not set — abort. ***")
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
log("== STEP 3: DTC off + CommunicationControl quiet ==")
|
|
194
|
+
_tx(h, target, DTC_OFF, "85 02 DTC off", log)
|
|
195
|
+
_tx(h, target, COMM_QUIET, "28 03 01 CommControl quiet", log)
|
|
196
|
+
|
|
197
|
+
log("== STEP 4: SetExtendedMode ==")
|
|
198
|
+
r = _tx(h, target, EXT_SET_01, "31 01 1003 01 SetExtMode", log)
|
|
199
|
+
if r is None or is_neg(r):
|
|
200
|
+
log(" *** SetExtendedMode rejected — abort. ***")
|
|
201
|
+
return False
|
|
202
|
+
|
|
203
|
+
log("== STEP 5: programmingSession (HU self-reboots to bootloader) ==")
|
|
204
|
+
_tx(h, target, EXT_SESSION, "10 03", log)
|
|
205
|
+
r = _tx(h, target, PROG_SESSION, "10 02 ProgrammingSession", log)
|
|
206
|
+
if r is not None and is_neg(r) and len(r) > 2 and r[2] == 0x22:
|
|
207
|
+
log(" *** 10 02 conditionsNotCorrect — abort. ***")
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
log(" [..] HU self-rebooting — polling 22 F186 (<= %ds)" % reset_timeout)
|
|
211
|
+
try:
|
|
212
|
+
h.close()
|
|
213
|
+
except Exception:
|
|
214
|
+
pass
|
|
215
|
+
t0 = time.time()
|
|
216
|
+
sess = None
|
|
217
|
+
while time.time() - t0 < reset_timeout:
|
|
218
|
+
time.sleep(1.0)
|
|
219
|
+
try:
|
|
220
|
+
h.reconnect(timeout=2.0)
|
|
221
|
+
r = h.request(target, bytes([0x22, 0xF1, 0x86]))
|
|
222
|
+
if _ispos(r) and r[0] == 0x62 and len(r) >= 4:
|
|
223
|
+
sess = r[3]
|
|
224
|
+
log(" [..] HU answering after %.0fs: session 0x%02X" % (time.time() - t0, sess))
|
|
225
|
+
break
|
|
226
|
+
except Exception:
|
|
227
|
+
try:
|
|
228
|
+
h.sock.close()
|
|
229
|
+
except Exception:
|
|
230
|
+
pass
|
|
231
|
+
h.reconnect()
|
|
232
|
+
if sess == 0x02:
|
|
233
|
+
log(" *** HU in PROGRAMMING SESSION (bootloader). ***")
|
|
234
|
+
return True
|
|
235
|
+
if sess is not None:
|
|
236
|
+
log(" HU in session 0x%02X (not bootloader). ***" % sess)
|
|
237
|
+
else:
|
|
238
|
+
log(" *** HU did not answer within %ds. ***" % reset_timeout)
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
# ------------------------------------------------------------------------------- teardown
|
|
243
|
+
|
|
244
|
+
def teardown(h: Hsfz, target: int = TGT_HU, log: Callable = print) -> bool:
|
|
245
|
+
"""Restore the HU to the normal app session after a flash or abort."""
|
|
246
|
+
log("== TEARDOWN: restore HU to app ==")
|
|
247
|
+
try:
|
|
248
|
+
h.reconnect()
|
|
249
|
+
except Exception:
|
|
250
|
+
pass
|
|
251
|
+
_tx(h, target, ACT_APP_MODE, "31 01 100F ActAppMode", log)
|
|
252
|
+
|
|
253
|
+
en_ok = ext_ok = False
|
|
254
|
+
st = None
|
|
255
|
+
for _ in range(6):
|
|
256
|
+
try:
|
|
257
|
+
h.reconnect()
|
|
258
|
+
except Exception:
|
|
259
|
+
pass
|
|
260
|
+
_tx(h, target, EXT_SESSION, "10 03", log)
|
|
261
|
+
_tx(h, target, ENERGY_OFF, "31 01 0F0C 00 EnergyOff", log)
|
|
262
|
+
en_ok = _val(_tx(h, target, bytes([0x22, 0x10, 0x0A]), "22 100A", log)) == 0x00
|
|
263
|
+
_tx(h, target, EXT_SET_OFF, "31 01 1003 00 ExtModeOff", log)
|
|
264
|
+
ext_ok = _val(_tx(h, target, bytes([0x22, 0x10, 0x0E]), "22 100E", log)) == 0x00
|
|
265
|
+
st = _read_session(h, target, "22 F186", log)
|
|
266
|
+
if (en_ok and ext_ok) or st == 0x01:
|
|
267
|
+
break
|
|
268
|
+
time.sleep(2.0)
|
|
269
|
+
|
|
270
|
+
_tx(h, target, COMM_NORMAL, "28 00 01 CommNormal", log)
|
|
271
|
+
_tx(h, target, DTC_ON, "85 01 DTC on", log)
|
|
272
|
+
|
|
273
|
+
if (en_ok and ext_ok) or st == 0x01:
|
|
274
|
+
_tx(h, target, HARD_RESET, "11 01 reset -> app", log)
|
|
275
|
+
return True
|
|
276
|
+
log(" *** not clean — forcing reset ***")
|
|
277
|
+
_tx(h, target, HARD_RESET, "11 01 reset (force)", log)
|
|
278
|
+
try:
|
|
279
|
+
h.close()
|
|
280
|
+
except Exception:
|
|
281
|
+
pass
|
|
282
|
+
t0 = time.time()
|
|
283
|
+
while time.time() - t0 < 25:
|
|
284
|
+
time.sleep(2.0)
|
|
285
|
+
try:
|
|
286
|
+
h.reconnect()
|
|
287
|
+
if _read_session(h, target, "22 F186 (after reset)", log) == 0x01:
|
|
288
|
+
log(" [..] back in app after %.0fs" % (time.time() - t0))
|
|
289
|
+
return True
|
|
290
|
+
except Exception:
|
|
291
|
+
try:
|
|
292
|
+
h.sock.close()
|
|
293
|
+
except Exception:
|
|
294
|
+
pass
|
|
295
|
+
log(" *** HU did not return to app after reset. ***")
|
|
296
|
+
return False
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
# ------------------------------------------------------------------------------- transfer + auth
|
|
300
|
+
|
|
301
|
+
def _xfer_block(h: Hsfz, target: int, seq: int, chunk: bytes) -> tuple[bool, Optional[str]]:
|
|
302
|
+
frame = bytes([SID_TD, seq & 0xFF]) + chunk
|
|
303
|
+
for attempt in range(6):
|
|
304
|
+
try:
|
|
305
|
+
h.send_only(target, frame)
|
|
306
|
+
while True:
|
|
307
|
+
_ctrl, body = h.recv_frame(timeout=8.0)
|
|
308
|
+
resp = body[2:] if len(body) > 2 else b""
|
|
309
|
+
if not is_neg(resp) and len(resp) >= 2 and resp[0] == 0x76:
|
|
310
|
+
if resp[1] == seq:
|
|
311
|
+
return True, ("resent" if attempt else None)
|
|
312
|
+
continue
|
|
313
|
+
if is_neg(resp):
|
|
314
|
+
nrc = resp[2] if len(resp) > 2 else 0
|
|
315
|
+
if nrc == 0x73:
|
|
316
|
+
return True, "0x73-advance"
|
|
317
|
+
if nrc in (0x21, 0x7F):
|
|
318
|
+
time.sleep(0.2)
|
|
319
|
+
break
|
|
320
|
+
return False, "NRC 0x%02X %s" % (nrc, NRC_NAMES.get(nrc, "?"))
|
|
321
|
+
except TimeoutError:
|
|
322
|
+
time.sleep(0.3)
|
|
323
|
+
except Exception:
|
|
324
|
+
time.sleep(0.4)
|
|
325
|
+
return False, "exhausted retries"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _l3_auth(h: Hsfz, target: int, key: dict, btld: int, user_id: int,
|
|
329
|
+
log: Callable) -> bool:
|
|
330
|
+
log("\n== SecurityAccess L3 (flash) ==")
|
|
331
|
+
u = Uds(h, target) # same facade coding/fsc use, levels 0x11/0x12
|
|
332
|
+
try:
|
|
333
|
+
seed = u.request_seed(SA_SEED_FLASH, user_id)
|
|
334
|
+
except Exception as e:
|
|
335
|
+
log(" *** no seed — abort, nothing written. (%s) ***" % e)
|
|
336
|
+
return False
|
|
337
|
+
sig = l3.sign_seed(seed, key, btld=btld, user_id=user_id)
|
|
338
|
+
try:
|
|
339
|
+
u.send_key(SA_KEY_FLASH, sig)
|
|
340
|
+
except Exception as e:
|
|
341
|
+
log(" *** auth NOT granted — abort, nothing written. (%s) ***" % e)
|
|
342
|
+
return False
|
|
343
|
+
log(" *** L3 auth GRANTED ***")
|
|
344
|
+
return True
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ------------------------------------------------------------------------------- apply_flash
|
|
348
|
+
|
|
349
|
+
def apply_flash(h: Hsfz, img: bytes, *, target: int = TGT_HU, reset_timeout: int = 50,
|
|
350
|
+
key: dict, btld: int = 0x00001FFD, lbn: int = 0,
|
|
351
|
+
fingerprint: bytes = b"\x00" * 13, user_id: int = 0x00000000,
|
|
352
|
+
log: Callable = print) -> bool:
|
|
353
|
+
"""Flash a SWDL container to the HU: enter bootloader → L3 auth → transfer → finalize.
|
|
354
|
+
|
|
355
|
+
Returns True on successful transfer commit. The caller is responsible for teardown.
|
|
356
|
+
"""
|
|
357
|
+
if not enter_progmode(h, target, reset_timeout, log=log):
|
|
358
|
+
log("\n *** did not reach bootloader — ABORT, NOTHING written. ***")
|
|
359
|
+
return False
|
|
360
|
+
|
|
361
|
+
if not _l3_auth(h, target, key, btld, user_id, log):
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
h.start_tester_present(log=log)
|
|
365
|
+
|
|
366
|
+
log("\n== SWDL write (%d bytes) ==" % len(img))
|
|
367
|
+
# fingerprint (optional, may NRC)
|
|
368
|
+
try:
|
|
369
|
+
h.request(target, bytes([SID_WDBI, DID_FINGERPRINT >> 8, DID_FINGERPRINT & 0xFF]) + fingerprint)
|
|
370
|
+
except Exception:
|
|
371
|
+
pass
|
|
372
|
+
|
|
373
|
+
# erase
|
|
374
|
+
r = _tx(h, target, _fr_erase(lbn), "31 FF00 eraseMemory", log)
|
|
375
|
+
if not _ispos(r):
|
|
376
|
+
log(" *** erase rejected — ABORT (live file untouched). ***")
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
# request download
|
|
380
|
+
r = _tx(h, target, _fr_req_download(0, len(img)), "34 requestDownload", log)
|
|
381
|
+
if not _ispos(r):
|
|
382
|
+
log(" *** requestDownload rejected — ABORT. ***")
|
|
383
|
+
return False
|
|
384
|
+
block_size = _parse_block_length(r)
|
|
385
|
+
nblocks = (len(img) + block_size - 1) // block_size
|
|
386
|
+
log(" block size %d, %d blocks" % (block_size, nblocks))
|
|
387
|
+
|
|
388
|
+
# transfer
|
|
389
|
+
seq = 1
|
|
390
|
+
t0 = time.time()
|
|
391
|
+
recovered = 0
|
|
392
|
+
for i, off in enumerate(range(0, len(img), block_size)):
|
|
393
|
+
chunk = img[off:off + block_size]
|
|
394
|
+
ok, note = _xfer_block(h, target, seq, chunk)
|
|
395
|
+
if not ok:
|
|
396
|
+
log(" *** block %d/%d FAILED: %s — ABORT. ***" % (i + 1, nblocks, note))
|
|
397
|
+
return False
|
|
398
|
+
if note:
|
|
399
|
+
recovered += 1
|
|
400
|
+
if i % 50 == 0 or i == nblocks - 1:
|
|
401
|
+
sent = off + len(chunk)
|
|
402
|
+
elapsed = max(0.001, time.time() - t0)
|
|
403
|
+
log(" [..] block %d/%d (%.0fs, %.1f KB/s, %d recovered)"
|
|
404
|
+
% (i + 1, nblocks, elapsed, sent / 1024.0 / elapsed, recovered))
|
|
405
|
+
seq = (seq + 1) & 0xFF
|
|
406
|
+
log(" transfer complete: %d blocks, %.1fs" % (nblocks, time.time() - t0))
|
|
407
|
+
|
|
408
|
+
# finalize
|
|
409
|
+
r = _tx(h, target, bytes([SID_TX]), "37 transferExit", log)
|
|
410
|
+
if not _ispos(r):
|
|
411
|
+
log(" *** transferExit rejected. ***")
|
|
412
|
+
return False
|
|
413
|
+
|
|
414
|
+
_tx(h, target, _fr_check(lbn), "31 0202 checkMemory", log)
|
|
415
|
+
_tx(h, target, bytes([SID_ROUTINE, 0x01, RID_DEPCHECK >> 8, RID_DEPCHECK & 0xFF]),
|
|
416
|
+
"31 FF01 checkProgDeps", log)
|
|
417
|
+
|
|
418
|
+
tgt, _nm, is_live = container_target(img)
|
|
419
|
+
if is_live:
|
|
420
|
+
log("\n *** WRITE SENT — LIVE-DIRECT to %s. Goes live next boot. ***" % (tgt or "?"))
|
|
421
|
+
else:
|
|
422
|
+
log("\n *** WRITE SENT — staged at %s. ***" % (tgt or "?"))
|
|
423
|
+
return True
|
evocode/uds.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""UDS (ISO 14229) over HSFZ — only the services coding needs, plus SVK (``22 F1 01``) decoding.
|
|
3
|
+
|
|
4
|
+
The SVK is how you learn a module's bootloader (BTLD) and coding dataset (CAFD) ids and versions;
|
|
5
|
+
almost everything (which signing key, which coding layout) keys off those. Read it first.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from typing import List, Optional
|
|
11
|
+
|
|
12
|
+
from .hsfz import Hsfz, TGT_HU
|
|
13
|
+
|
|
14
|
+
# service ids
|
|
15
|
+
SID_SESSION = 0x10
|
|
16
|
+
SID_RESET = 0x11
|
|
17
|
+
SID_SECURITY = 0x27
|
|
18
|
+
SID_READ_DID = 0x22
|
|
19
|
+
SID_WRITE_DID = 0x2E
|
|
20
|
+
SID_ROUTINE = 0x31
|
|
21
|
+
|
|
22
|
+
# identifiers used here
|
|
23
|
+
DID_SVK = 0xF101
|
|
24
|
+
DID_VIN = 0xF190
|
|
25
|
+
|
|
26
|
+
# The canonical UDS negative-response names. swdl.py builds on this (adds flash-specific NRCs).
|
|
27
|
+
NRC_NAMES = {
|
|
28
|
+
0x10: "generalReject", 0x11: "serviceNotSupported",
|
|
29
|
+
0x13: "incorrectMessageLengthOrInvalidFormat", 0x22: "conditionsNotCorrect",
|
|
30
|
+
0x31: "requestOutOfRange", 0x33: "securityAccessDenied", 0x35: "invalidKey",
|
|
31
|
+
0x36: "exceedNumberOfAttempts", 0x7E: "subFunctionNotSupportedInActiveSession",
|
|
32
|
+
0x7F: "serviceNotSupportedInActiveSession",
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# SVK process classes (first byte of each 8-byte entry) — the ones coding cares about named.
|
|
36
|
+
_SVK_CLASS = {1: "HWEL", 2: "HWAP", 3: "HWFR", 4: "GWTB", 5: "CAFD", 6: "BTLD", 7: "FLSL",
|
|
37
|
+
8: "SWFL", 9: "SWFF", 10: "SWPF", 11: "ONPS"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class UdsError(Exception):
|
|
41
|
+
def __init__(self, sid: int, nrc: int):
|
|
42
|
+
self.sid, self.nrc = sid, nrc
|
|
43
|
+
super().__init__("UDS 0x%02X negative response: 0x%02X (%s)" % (sid, nrc, NRC_NAMES.get(nrc, "unknown")))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _positive(sid: int, resp: bytes) -> bytes:
|
|
47
|
+
if len(resp) >= 3 and resp[0] == 0x7F and resp[1] == sid:
|
|
48
|
+
raise UdsError(sid, resp[2])
|
|
49
|
+
if not resp or resp[0] != sid + 0x40:
|
|
50
|
+
raise RuntimeError("unexpected UDS response to 0x%02X: %s" % (sid, resp.hex()))
|
|
51
|
+
return resp
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class SgbmId:
|
|
56
|
+
kind: str
|
|
57
|
+
id: int
|
|
58
|
+
main: int
|
|
59
|
+
sub: int
|
|
60
|
+
patch: int
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def id_hex(self) -> str:
|
|
64
|
+
return "%08X" % self.id
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def version(self) -> str:
|
|
68
|
+
return "%03d.%03d.%03d" % (self.main, self.sub, self.patch)
|
|
69
|
+
|
|
70
|
+
def __str__(self) -> str:
|
|
71
|
+
return "%s-%s-%s" % (self.kind, self.id_hex, self.version)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass
|
|
75
|
+
class Svk:
|
|
76
|
+
version: int
|
|
77
|
+
prog_dep_checked: int
|
|
78
|
+
entries: List[SgbmId]
|
|
79
|
+
|
|
80
|
+
def of(self, kind: str) -> List[SgbmId]:
|
|
81
|
+
return [e for e in self.entries if e.kind == kind]
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def btld(self) -> Optional[SgbmId]:
|
|
85
|
+
got = self.of("BTLD")
|
|
86
|
+
return min(got, key=lambda e: (e.id, e.main, e.sub, e.patch)) if got else None
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def cafds(self) -> List[SgbmId]:
|
|
90
|
+
return self.of("CAFD")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def decode_sgbm(b: bytes, at: int = 0) -> SgbmId:
|
|
94
|
+
cls = b[at]
|
|
95
|
+
return SgbmId(_SVK_CLASS.get(cls, "C%02X" % cls),
|
|
96
|
+
int.from_bytes(b[at + 1:at + 5], "big"), b[at + 5], b[at + 6], b[at + 7])
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def decode_svk(value: bytes) -> Svk:
|
|
100
|
+
"""Decode the bytes after ``62 F1 01``.
|
|
101
|
+
|
|
102
|
+
Layout: ``[version u8][progDepChecked u8][count u16 BE][fingerprint (variable)][N*8 entries]``.
|
|
103
|
+
The fingerprint length varies, so the entries are taken from the final ``8*count`` bytes.
|
|
104
|
+
"""
|
|
105
|
+
if len(value) < 4:
|
|
106
|
+
raise ValueError("SVK too short: %d bytes" % len(value))
|
|
107
|
+
count = int.from_bytes(value[2:4], "big")
|
|
108
|
+
need = count * 8
|
|
109
|
+
if 4 + need > len(value):
|
|
110
|
+
raise ValueError("SVK announces %d entries but has only %d bytes" % (count, len(value)))
|
|
111
|
+
first = len(value) - need
|
|
112
|
+
entries = [decode_sgbm(value, first + i * 8) for i in range(count)]
|
|
113
|
+
return Svk(value[0], value[1], entries)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class Uds:
|
|
117
|
+
"""A thin UDS facade bound to one target over an :class:`~evocode.hsfz.Hsfz` session."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, hsfz: Hsfz, target: int = TGT_HU):
|
|
120
|
+
self.h = hsfz
|
|
121
|
+
self.target = target
|
|
122
|
+
|
|
123
|
+
def raw(self, uds: bytes) -> bytes:
|
|
124
|
+
return self.h.request(self.target, uds)
|
|
125
|
+
|
|
126
|
+
def session(self, sub: int) -> bytes:
|
|
127
|
+
return _positive(SID_SESSION, self.raw(bytes([SID_SESSION, sub])))
|
|
128
|
+
|
|
129
|
+
def reset(self, sub: int = 0x01) -> bytes:
|
|
130
|
+
return _positive(SID_RESET, self.raw(bytes([SID_RESET, sub])))
|
|
131
|
+
|
|
132
|
+
def request_seed(self, level: int, tester_id: int = 0x00000000) -> bytes:
|
|
133
|
+
# requestSeed carries the 4-byte testerId; a bare 27 xx returns NRC 0x13.
|
|
134
|
+
r = _positive(SID_SECURITY, self.raw(bytes([SID_SECURITY, level]) + tester_id.to_bytes(4, "big")))
|
|
135
|
+
return r[2:] # strip SID echo + level
|
|
136
|
+
|
|
137
|
+
def send_key(self, level: int, key: bytes) -> bytes:
|
|
138
|
+
return _positive(SID_SECURITY, self.raw(bytes([SID_SECURITY, level]) + key))
|
|
139
|
+
|
|
140
|
+
def read_did(self, did: int) -> bytes:
|
|
141
|
+
r = _positive(SID_READ_DID, self.raw(bytes([SID_READ_DID, did >> 8, did & 0xFF])))
|
|
142
|
+
return r[3:] # strip 62 + 2-byte DID echo
|
|
143
|
+
|
|
144
|
+
def write_did(self, did: int, data: bytes) -> bytes:
|
|
145
|
+
return _positive(SID_WRITE_DID, self.raw(bytes([SID_WRITE_DID, did >> 8, did & 0xFF]) + data))
|
|
146
|
+
|
|
147
|
+
def routine(self, sub: int, rid: int, data: bytes = b"") -> bytes:
|
|
148
|
+
return _positive(SID_ROUTINE, self.raw(bytes([SID_ROUTINE, sub, rid >> 8, rid & 0xFF]) + data))
|
|
149
|
+
|
|
150
|
+
def read_vin(self) -> str:
|
|
151
|
+
return self.read_did(DID_VIN).decode("latin1").strip("\x00 ")
|
|
152
|
+
|
|
153
|
+
def read_svk(self) -> Svk:
|
|
154
|
+
return decode_svk(self.read_did(DID_SVK))
|