🌵

Mission Control

Task-first workspace ·

openSAAB.com Live
Android native DTC read + clear verified
Collector v0.2.8-alpha (shim struct fix)
13 commands catalogued
20 ECUs named ($1A 97)
1,735 DTCs catalogued (WIS-sourced)
Both shims built (CSTech2Win + j2534)
33/33 Bojer Match
D-PDU corpus vendored (pdu_mdi2 + ISO22900.II)
pydpdu.py D-PDU wrapper built
Engine SAS path identified ($07E0/FD)
SAAB4WIN installer suite shipped (emulator patch = 55 bytes)
SPS wormcode cracked + .utl MD5-verified vs sas_saabdb
GMLAN command reference built (SpsVcs.dll RE)
Community mapped: Bojer/z90.pl + rogue33/saabiste
SX bench: $245 L1 seed + $27 0B visible
Two-door map locked: T8 L1 ≠ SAS $27 0B
Chipsoft cold gate: $0541 is a scoreboard, not the law
Next proof: $241/$641 GMLAN VIN
openSAAB.com DNS re-route pending

📌 Pinned projects

2 projects

Vehicle-tested: Original Tech2 and CANdi firmware run locally on Pixel 7 through our Rust core and direct Nano USB transport. The original guest drives diagnostic requests, receives real replies and draws the result screen. Screenshots, commands and reproducible findings →

5 DTCsOriginal read display
ClearedOriginal final screen
2 busesP-bus + I-bus transport

CANdi routing: Native channel 4 / controller 0 / 0x200000 → Nano 0 / 500 kbit/s P-bus. Native channel 0 / controller 2 / 0x202000 → Nano 1 / 33.3 kbit/s I-bus. Middle channel 2 is not enabled. Serial wake 88 0E 6A is distinct from CAN ID 0x100; J2534 wake flag 0x0400 maps to Nano USB flag 0x1000.

DTC flow: The read displayed BCM B0092-05, RRDM B3210-06 / B3205-06, TPMM C0777-5A and UEC B2575-04. ECM and TCS/ESP were missing in that scan. Raw CIM C0547-04 reached the guest filter but did not appear in its summary. The separate clear used 04 → 44, permitted the observed A9 81 0A status read, preserved NRC78 waits and followed the physical LOCK prompt to “Vehicle DTC’s Cleared.”

Original Android DTC read and separate clear completed; actual screenshots and raw evidence retained.
Console distinguishes native serial commands, CANdi TX, USB completion, real RX and guest filter acceptance. Both completed runs released USB cleanly.
Separate native SSA collection produced the original 714-byte pre-auth block with ten populated records; no API processing or completed unlock claimed.
Fresh post-clear DTC scan and repeatability across modules / ignition states. Clearing does not prove faults repaired.
Live engine values remain unvalidated; original 42-item page reached with placeholders. Investigation paused at the operator's request.
Validate Chipsoft Pro and MDI transports independently. iOS wireless remains a design option.

Tracked: Private Android app and shared Rust core; redacted findings in OpenSAAB's published notes. Proprietary firmware and security data are not distributed.

September 8 update: The original firmware now reads and clears DTCs on Android through direct Nano USB. This project retains the separate Chipsoft Pro / macOS goal; Nano success does not establish Chipsoft compatibility. Read the verified native results →

Goal: Read ECM identity and software information through Chipsoft Pro, first in a headless test and then from the Rust emulator GUI, with every request and response visible in the console.

USBLocal adapter connection
Read-onlyFirst ECU milestone
PlannedEmulator hardware validation

Bench path: M4 Mac mini → USB-C data cable / adapter → Chipsoft Pro → powered OBD-II bench harness → ECM. For an NG 9-3 / Trionic 8 target, begin with 500 kbit/s high-speed CAN (P-bus). Confirm the actual ECM part number, model year, connector pinout, supply, ignition feeds, grounds and CAN termination before connecting the bench.

Evidence available: USB captures, corrected Chipsoft frame decoding, a macOS probe scaffold, and Roffe's GoCAN / gateway references. These are a starting point; Chipsoft-specific native channel setup and the emulator-to-adapter bridge still need implementation and hardware validation. The separate Nano Android path has now completed native DTC workflows.

Protocol research and reference captures collected; macOS probe scaffold available.
Confirm the bench ECM and wiring; enumerate Chipsoft USB and record adapter identity.
Open the required CAN channel and perform a bounded, read-only ECU identity / VIN / software query.
Connect the same transport to the guest diagnostic path and demonstrate the query from the Rust GUI.
Log request → queue → USB write → adapter acknowledgement → ECU response with timestamps and raw bytes. Label live, replayed and simulated traffic separately.
Verify timeout, cancellation and USB-detach recovery. Keep the GUI responsive and return one screen when an adapter or program wait expires.
Chipsoft Android follow-on: reuse the shared Rust core and touch controls already working with Nano, then validate Chipsoft USB framing and electrical modes.

Android approach: Kotlin / Compose owns USB permission, device attachment and bulk I/O; JNI connects it to the shared Rust protocol and emulator core. Use worker threads, framed streaming reads, bounded queues and cancellable deadlines. The Windows DLL is a protocol reference; it does not run on Android. Keep diagnostic traffic local; use the OpenSAAB API separately for vehicle enrichment.

Next acceptance test: Save one timestamped headless session showing adapter identity, a real ECU response and clean recovery when the adapter is unplugged. Repeat from the GUI for Chipsoft; Nano Android results are tracked separately above.

Related: Chipsoft reverse engineering · Android application · GoCAN gateway reference

🔴 Now

21 active
🗺️ 2026-08-13 — Two-door map locked · isolated T8 is the wrong SAS lab · $0541 demoted to scoreboard
2026-08-13 15:15–16:54 PDT · Gateway back after a two-month journal gap · No new bench bytes · Architecture cleaned: T8 flash/$7E0 L1 is not SAS $0241 27 0B · Isolated ECM cannot open the I-bus door · Next proof is still Bojer's staged $241/$641 GMLAN VIN
Now
What today actually was: a map-correction session after the gateway came back. No new seed, no new pcap, no hardware change. Live Koyeb /dashboard was still the 2026-06-05 overlay. Identity stays VIN 2017 YS3FD49YX41012017 — do not mix unmatched used CIM/SCL/ISM onto that identity.

Product recenter: the app goal is still emulate/perform security access. The 3-module CIM/SCL/ISM harness kept hot 24/7 is a possible adapter soak, not a proven SAS oracle. Tester-side firmware (GlobalTIS / SPS / Tech2) is not the vehicle. Seed $27 0B is ECM/SAS on $0241. Golden prep knocks 8 SWCAN modules + ECM, not a 3-box IMMO loop.

Two-door map, locked: TrionicCANFlasher / T8 flash is L1 $27 01/02 on $7E0 via stgterm / Trionic Step.cs (algo 0x0339 etc.). SAS/IMMO is $27 0B/0C via dllsecurity (0367 / C4DC / 4EED). Live Trionic sessions show VIN, seed, and CEL because they read the bus — those values are not stored in the saved 1 MB flash. Re-scanned Desktop 2004_BCB_SAAB_93_ECM.bin (1,048,576 B, SHA256 555e14e4…105a2c): real T8 image, no VIN, no C4DC, no 4EED, no 714-byte SSA.

Isolated T8 lab, bounded: Chipsoft-on-Windows + TrionicCANFlasher is a real T8 path for VIN/info/DTC, L1 seeds, CIM L1 $245 27 01, SRAM/flash. Isolated T8 is HS-CAN 500k only — no SWCAN pin-1 @ 33.3. You cannot knock golden SAS $0241 27 0B from a lone T8 tester. $241 on P-bus ≠ pin-1 SAS. Golden $27 0B needs CIM/SAS on SWCAN or a dual-homed gateway.

$0541 demoted: we do not have to worship $0541. We have to beat NRC 22. $0541 is the best observed scoreboard, not a GM law. No cold $27 0B success exists without that ready/latched state. Real arounds: consume a latch (lab only), use L1 (wrong door), or find the actual cause Bojer pointed at (GMLAN VIN / session flags) and ignore $0541 if $27 0B then answers.

Bojer 3-device claim: workspace notes do not contain a quote that an ISM/CIM/SCL harness is valid for $27 0B. Closest written guidance: Tech2Win expects a full car so isolated ECM may fail; start with CIM on $241/$641 + Tester Present; do not unlock non-CIM modules yet. SCL is never mentioned. Paste the message if it exists.

Next ▶ do not lead with another $0541 replay or a 3-box IMMO soak. Immediate missing artifact is still the clean ECM GMLAN VIN proof over $241/$641 RDBI $90, then CIM VIN with Tester Present, then I-bus, then seed/key. Use reports/firmware-re/bojer_alignment_audit_20260617.md and reports/firmware-re/j2534_python_pro_bringup_20260616.md.
🧭 2026-06-04 — $0541 cold gate narrowed · RAM readiness predicate recovered · CDC lifecycle test staged
2026-06-04 10:55-20:05 HST · OBDLink SX mapped the HS-CAN boundary, Chipsoft csdirect.py fixed BLOCK filters, and golden timing proved a favorable-state path to 67 0B C4 DC · Cold retests kept $0541 at * 06 02 02 * and 1A 3F → 7F 1A 22 · Shim-ladder and exact pre-wake bulk replay both failed · Correct RAM decode recovered not-ready, ready, and latched $0541 rows · No 27 0C unlock was sent
Decision
What the SX tests proved: OBDLink SX / ELM over HS-CAN reaches CIM $245/$645 cleanly. $245 27 01 returns 67 01 F6 31. The important surprise: $245 27 0B is not silent; it returns 7F 27 78 then 7F 27 22. That means the 0B door is visible through the HS-CAN route, but the security/session precondition is missing.

Non-AE ladder result: $245 AA 01 01, $245 1A 90, $245 1A 9A, $245 1A 3F, and a minimal HS L1 walk did not satisfy the 0B condition. The walk still expanded the map: $245 27 01 → F631, $7E0 27 01 → 655D, and $7E1 27 01 → D2DC. SX can pull multiple HS L1 seeds; it just cannot cold-open the SAS/IMMO 0B gate with simple physical prep.

Chipsoft direct result: Desk diff found the failed pregate bug: golden Tech2Win installs BLOCK filters with third length 00, while csdirect.py used 04, causing all 82 BLOCK filters to fail with ERR_0x6E. After patching BLOCK length to 00, clean-hot retests proved AE 00 → EE 00 alone was insufficient: fast golden replay still returned hard 7F 1A 22 and withheld $27 0B.

Timed-gate proof: timestamp diff showed the replay was byte/count-correct but too fast: wake → 1A 3F was about 3.0s vs Tech2Win's 11.2s, and AE → 1A 3F was about 63ms vs Tech2Win's 253ms. With -pregate-golden-timing, the hot bench reached AE 00 → EE 00, 1A 3F → 7F 1A 78, extra read-only wait → 5A 3F + VIN, then $27 0B → 7F 27 78 → 67 0B C4 DC. Local security_calc mapped seed C4DC to key 4EED. No 27 0C unlock was sent.

Cold-gate retest: after bench power-cycle, timedgate_cold still reached AE 00 → EE 00, but $0541 stayed in the not-ready DPID/UUDT shape 01 7c 06 02 02 fd 04 fc and $0241 1A 3F → 7F 1A 22; $27 0B was withheld. EE-overlap, L1 clear, key-status prelude, $20 ReturnToNormal, and waiting 420 reads for $0541 ready all failed.

Follow-up probes: desk parse of golden showed the final $0241 AA 01 01 after the HS/SW functional FE 3E pair flips $0541 from 01 7d 06 02 02 fd 04 fc to ready 01 7e 01 01 00 fd 38 fc. Live AA pumping, multi-DPID AA, FE3E+AA pumping, paced read replay, 420-read waits, guarded post-Tech2Win latch checks, three guarded shim-derived ladder replays, Tech2Win open-prologue replay, and exact pre-wake bulk replay did not reproduce that state. Closing Tech2Win enough to free COM7 still returned hard 7F 1A 22; the favorable state was not just process-latched.

Tri-diff result: tri_diff_gate_evidence_20260604.md aligns Tech2Win RAM, the golden USBPcap, failed cold USBPcap, and CSTech2Win shim logs. Golden and shim sessions move $0541 to * 01 01 00 * before AE 00; failed cold sessions stay at * 06 02 02 *. The visible golden bridge is $0101 FE 3E proto 0x0006, $0101 FE 3E proto 0x8007, then $0241 AA 01 01, but standalone FE3E+AA pumping and shim-ladder replay left $0541 pinned at 01 78 06 02 02 fd 04 fc.

RAM correction: the full-memory dump does preserve $0541 readiness once the 16-byte record is decoded as payload8 = data4 + tail[0:4]. analyze_0541_memdump.py reconstructed 73 $0541 rows: 9 not-ready 06 02 02, 5 ready 01 01 00, and 1 latched 00 00 00. That makes $0541 == * 01 01 00 * an independently confirmed Tech2Win RAM predicate, not just a pcap/shim logging artifact.

Exact prewrite result: exactprewrite3 replayed the golden bulk OUT stream from CONNECT through final pre-wake READ_MSG byte-for-byte: 1,775 commands, 996 filters, 684 reads, and 1,787/1,787 prewrite bulk OUT frames matched. It still reached AE 00 → EE 00, then hard-failed 1A 3F → 7F 1A 22; $0541 stayed not-ready and $27 0B/$27 0C were not sent.

Current blocker: the remaining raw-USB delta is CDC/open lifecycle: golden starts with line coding 115200 8N1, sets 38400 8N1, toggles DTR/RTS, and runs the first two command-layer open cycles across real COM handle open/close boundaries before the main third-open path. Next bench command is cdc115200_cdcprologue_exactprewrite_require0541 with -CdcPrecondition115200 -CdcGoldenOpenPrologue -Require0541Ready -GoldenTiming -ExactPrewritePcap. First pass/fail is still $0541 == * 01 01 00 * before AE.

D-PDU fallback now staged: if the CDC discriminator fails, run pydpdu/CSTech2Win with the monitor-CoP re-arm fix. The active pydpdu.py now re-issues the persistent num_recv=-1 IS-CYCLIC monitor before every data send and before direct 1A 3F, on physical and functional CLLs, and withholds AE 00 unless $0541 final-ready is observed. Test N passes locally and on .80. Bench command: run_pydpdu_capture.ps1 -TimeoutSec 90 -Tag monitor_rearm.

2026-06-11 update — shim solved seed at D-PDU boundary: CSTech2Win shim v6 already extracts live 67 0B C4 DC via PDUGetEventItem.pData dereference on 0x1301 STATUS items. Frida was re-solving a solved layer (WOW64 pointer bug). Active edge is pydpdu transition-unit with TU1_WAKE_ATTEMPTS=40 (sustained AA until first $0641 reply). Staged to .80. Next: re-run start_pydpdu_transition_unit_multicll.bat on live bench.

Artifacts: timed-gate success csdirect_pregate_timedgate_20260604-185356.pcap; cold failure csdirect_pregate_timedgate_cold_20260604-194106.pcap; shim-ladder failures csdirect_pregate_shim0541_hot1_20260604-211942.pcap, shim0541_hot2, and shim0541_hot3; exact bulk replay failure csdirect_pregate_exactprewrite3_20260604-214417.pcap; RAM readiness report reports/firmware-re/tech2win_memdump_0541_readiness_20260604.md; CDC/control diff reports/firmware-re/usb_control_prewrite_diff_20260604.md; CDC lifecycle recipe reports/firmware-re/cdc_open_lifecycle_discriminator_20260604.md; pydpdu root-cause reports/firmware-re/pydpdu_result_routing_rootcause.md; all-tests reference reports/firmware-re/bench_test_reference_20260604.md.

Related memory: wiki/projects/android-j2534-driver.md, memory/2026-06-04.md, reports/firmware-re/csdirect_adapter_gate_spec.md, reports/firmware-re/tri_diff_gate_evidence_20260604.md
🧩 2026-05-30 — SAAB4WIN installer suite shipped · tpw_rules emulator patch decoded (55 bytes) + script/Inno single-exe installer + NoMoreGlobal v55 co-install
2026-05-30 21:42-23:42 HST · New private repo github.com/djfremen/SAAB4WIN · Diffed GM Tech2Win emulator.exe stock vs SAAB-patched (the tpw_rules community patch) · Built a reproducible installer that applies the patch without replacing the binary, co-installs Chris's NoMoreGlobal v55, and packages as one Inno Setup .exe · Full install/uninstall lifecycle validated on .80
Shipped
The patch, decoded: stock emulator.exe (MD5 d1f05a01…) → SAAB build (f054bd60…) differs by exactly 55 bytes / 5 clusters (ImageBase 0x400000). Author signs it at file 0x113660 ("This patch by tpw_rules…"). Cluster map: 0x58AB0/0x91FD1/0x92E68 = license+VM patches; 0x64C5A (7 bytes) = the I-Bus "communication fix" for 2003+ 9-3s (force mode=2 + swap a .data descriptor). Proof: the Desktop emulator OTHER.exeemulator SAAB.exe diff is only the 0x64C5A cluster, which pins it as the comms fix.

Installer suite (no binary swap): Install-Tech2WinSaab.ps1 byte-patches in place (backup + MD5 verify, idempotent) + installs the Saab NAO card/config + a Desktop shortcut that launches straight into the Saab config (reverse-engineered the emulator.exe flag invocation, bypassing the T2Configurator picker). Setup-SAAB4WIN.ps1 orchestrates the GM OEM installer (silent /quiet) → patch → NoMoreGlobal in one pass. Uninstall-SAAB4WIN.ps1 reverse-applies the 55-byte table to restore stock (works with no backup; stops a running NoMoreGlobal first). inno/SAAB4WIN.iss bundles everything into a 27 MB double-click installer with an Add/Remove-Programs uninstaller.

Suite = Tech2Win (SAAB-patched) + NoMoreGlobal v55 running in tandem — v55 is Chris's C# app hitting the Bojer web API for security access, so no VCI contention. JohnJocke/dpdu T7 driver considered then deferred out of scope.

Validation on .80: compiled (Inno 6.7.1); silent install + silent uninstall both clean; emulator round-trips patched↔stock by MD5; bench restored byte-for-byte (caught + fixed a file-lock bug — uninstaller now stops a running NoMoreGlobal). Only unproven branch = the silent OEM MSI install on a clean box (Chris tests on real HW). Built installer left at .80 Desktop.

Related memory: [[project-saab4win-installer-suite]], [[reference-windows-bench-80]]
🦜 2026-05-29 — Tech2Win-Parrot final assessment · driver layer fully solved · blocker is an upstream gate INSIDE Tech2Win, unreachable by a D-PDU/J2534 parrot
2026-05-29 · 9 test runs + RE, documented in Tech2Win-Parrot/docs/FINAL_ASSESSMENT_2026-05-29.md + TEST_LOG_2026-05-29.md (our most recent Parrot documentation) · Binary goal (car-less Tech2Win driving a recorded UDS session end-to-end) not achieved — but every driver-layer problem was solved; the remaining wall lives in Tech2Win's own logic
Assessed
What WORKS (banked): driver builds/deploys (Release|x86) + appears in Tech2Win's VCI list; clears E668561 via synthetic mode (no hardware); replays a 101-exchange recording (roster + 12-module VIN walk + seed 67 0b c4 dc); synthetic frames genuinely reach the emulator (Synthetic - RX ch3 deliver $645); AcceptanceId matching deep-copies T2W's expected-response array and stamps the right id (Test G).

The wall: across all 9 runs Tech2Win never escalates to diagnostic reads — in get-seed it transmits only fe 3e TesterPresent + an empty 0x100 probe, sets up monitor primitives, waits, tears down. It sends 1a 90/27 0b zero times, so the VIN/seed replies sitting ready are never requested. Nothing we changed at the driver/recording/bus layer altered what Tech2Win chooses to transmit → the decision is an upstream gate inside Tech2Win, before any reply of ours matters.

Two gate classes (key structural finding): (1) self-test / hardware checks (F2, MUX, ADC, DLC) route through emulated m68k firmware inside emulator.exehard wall, parrot never consulted (Test I: F2 made zero driver calls, failed on scenlib.c "no loopback connector"). (2) diagnostic apps (ECU-ID, DTCs, get-seed) DO route through the parrot but are gated by T2W's module-presence logic.

The one untried in-class experiment: we only tested get-seed (the hardest diagnostic — security access). Plain ECU-Identification (the lightest diagnostic) was never tried — that's the next experiment to see whether the diagnostic gate can be opened at all.

Related memory: [[project-tech2win-parrot]], [[project-saab-vcilib-1124-is-firmware-side]]
🎯 2026-05-28 — csdirect 7F 1A 22 root cause found + fix applied (staged for bench) · pre-1A-3F tester-present was the culprit
2026-05-28 20:34 HST · Cowork audit agent traced the cold-start 7F 1A 22 (conditionsNotCorrect) on 1A 3F to a tester-present (FE 3E) injected in a window the golden Tech2Win flow keeps silent · Verified to the timestamp against the 5/21 golden shim log · 2-line fix applied to warmup_1a3f() · NOT yet bench-tested
Staged
Root cause (verified, not inferred): The golden log 2026-05-21-bench-tech2win-copctrl.log shows Tech2Win keeps the EE 00 → 1A 3F window silent — last FE 3E at t=42738, AE 00 at 44405, EE 00 at 44637, 1A 3F fired bare at 44794, FE 3E resuming only at 45086 (~2.0s quiet gap). Tech2Win fires 1A 3F bare, takes the 7F 1A 78 pending, THEN resumes tester-present.

The bug: A 2026-05-26 change to warmup_1a3f() injected keepalive_both(cs) (an FE 3E on both SWCAN sides) immediately before the 1A 3F write — justified by a comment claiming "the 5/21 golden shim shows continuous FE 3E through the AE 00 → 1A 3F window." That was a misread of the log — the window is quiet for ~2s. Worse, the code's own 5/24 bench note 15 lines below warns that ANY FE 3E in that window flips the SAS NRC from 0x78 (responsePending) to 0x22 (conditionsNotCorrect) — i.e. csdirect was doing the exact thing its own evidence said causes the failure.

Fix applied (2 lines removed): Deleted the pre-write keepalive_both(cs) + sleep(0.05) in warmup_1a3f(). Left the post-pending keepalive in the poll loop (fires only after 7F 1A 78, matching the golden cadence). Single-variable discipline: the secondary suspect (send_ae00_preamble's keepalive in the AE 00 → EE 00 wait loop) is left in place — escalate only if this isn't enough.

Confirm/kill on next bench session: run csdirect.py on .80 with Chipsoft + bench car. Confirmed if 1A 3F now returns 7F 1A 78 then 5A 3F (then chase 27 0B → seed). Falsified if still 7F 1A 22 → escalate to the secondary keepalive, then to the continuous filter re-arm theory (Tech2Win issues ~996 SET_FILTER/session vs csdirect's 32 at boot).

Provenance note: This fix came from the Cowork audit agent's csdirect handoff (~/Desktop/HANDOFF_csdirect_7F1A22_2026-05-28.md). Every code + log claim independently verified against disk before applying — unlike the same agent's earlier repo-hygiene audit, which had 2 factual errors (a contradicted exposure claim and a missing-files claim that didn't hold up). Bench-RE reasoning: trustworthy. Provenance/hygiene claims: double-check.

Related memory: [[project-saab-prep-sequence-before-27-0b]] (updated with the FE 3E window finding)
🔧 2026-05-26 PM — dpdu-passthru bench iteration (8 runs) · 9 fixes shipped · jthread deadlock found · vci-error blocker persists, latest fix UNTESTED
2026-05-26 12:00-15:57 HST · Custom D-PDU driver replacing CSTech2Win.dll · Tech2Win consistently fails with vcilib.c 1124 1 dialog after driver selection · 9 fixes landed (NULL checks, refcount, deferred ComParam, silent filter accept, real PDUGetVersion, STATUS_ONLINE emit) · Latest hypothesis ad81675: std::jthread destructor deadlock on Connect re-entry · All 5 bench logs in R2 (X-Capture-Source: dpdu_passthru) · Bench paused after Chris went cold; MD5 378B66C5 deployed, untested
Paused
Session arc: Picked up the dpdu-saab custom D-PDU driver fork (replaces CSTech2Win.dll, calls Chipsoft's j2534_interface.dll underneath). 8 bench iterations on .80 ran Tech2Win against our driver; each iteration's log diagnosed one layer of bug; each fix cleared that layer but the vcilib.c 1124 1 dialog persisted. Latest fix ad81675 is the highest-probability real fix and is deployed but untested.

9 bugs shipped today:
  1. c631701 NULL-check m_eventCallbackFnc in SignalEvents — fixed access violation crash
  2. 1ada2a0 Refcount shared channelID across sibling CLLs — fixed ERR_INVALIDCHANNEL cascade when a sibling disconnects
  3. b673b5d Buffer SetComParam pre-Connect + skip Disconnect if never opened — fixed SET_CONFIG spam
  4. 803b0a4 StartMsgFilter silently accept ISO15765 filter requests + return synthetic FilterId — fixed ERR_ONLY_FLOWCONTROL_FILTER_TYPE_NEED
  5. b56e48a NULL-guard pOutputData in PDUIoCtl — fixed crash at driver.dll+0xfe70
  6. aeefec1 + 4b88348 PDUGetVersion returns real MVCI Part 2 = 0x02020000 (2.2.0) + populated HW/FW/Vendor strings
  7. daea2d8 Emit PDU_CLLST_ONLINE status event on Connect reuse path (sibling CLLs sharing channelID)
  8. 0954f31 REUSE: diagnostic probes around the STATUS_ONLINE emit — confirmed code wasn't executing despite source containing it
  9. ad81675 Connect short-circuits when m_running — fixes std::jthread destructor deadlock on re-entry
Smoking gun for the deadlock: Connect() can be entered twice per CLL — explicit PDUConnect after an implicit Connect() from StartMsgFilter. The reuse-path body reassigns m_runLoop = std::jthread(...), which destroys the old jthread. The destructor calls request_stop() + join(), but our run() loop checks m_running (not std::stop_token), so join() blocks forever. The bench log showed ChannelId 1 RunLoop started... (from the new thread) appear, then NO main-thread log activity — main thread was stuck inside the destructor. Fix: short-circuit Connect if m_running, just re-emit the ONLINE status event.

Process lesson — MSBuild incremental builds serve stale .obj: Same source SHA 8c75d5d produced two completely different binaries: incremental 52921343 vs clean rebuild f28e6ff9. Spent ~30 min misdiagnosing the runtime against code I'd already edited. Force /t:Rebuild after every code change. Verify with strings driver.dll | grep <unique-token>.

Infrastructure shipped:
  • dpdu-saab/bench_runs/ — per-iteration archive on Mac, date/sha/outcome-tagged filenames
  • bench_runs/deploy_and_archive.sh — staged .ps1 deploy (avoids SSH-bash-PS quote mangling), gzips + uploads to R2 via /ingest/shim-log
  • saab-security-api 578acbbINGEST_VALID_SOURCES extended to accept dpdu_passthru; Koyeb redeployed
  • kill_all_t2w.ps1 on .80 desktop — covers named Tech2Win/emulator/T2Configurator processes + any process loading driver.dll / CSTech2Win.dll / j2534_interface.dll + Tech2Win* services
  • All 5 bench iteration logs uploaded to R2 with X-Capture-Source: dpdu_passthru and X-Outcome tags
Push-pin LED observation: Chris noted Tech2Win's red push-pin (active comm indicator) stopped illuminating in later iterations. Consistent with the deadlock — Tech2Win treats the link as non-active because the post-Connect ONLINE event never arrived.

Next session pickup:
  1. Run Tech2Win on .80 with MD5 378B66C5 already deployed
  2. If still vci-error: force /t:Rebuild on .59 (incremental-build gotcha) and re-deploy
  3. Verify Connect - already running ... — re-emit ONLINE log line appears
  4. If still failing: investigate PDUSetUniqueRespIdTable (STUB, called ~40×) and adapter firmware mode (j2534_pro.bin vs kline_pro.bin vs canhacker_pro.bin)
Related memories: [[project-saab-dpdu-passthru-iso15765]], [[feedback-msbuild-incremental-serves-stale-obj]]
🔍 2026-05-25 PM — Golden shim log walked end-to-end · two prior-agent claims corrected · "missing 27 02" theory RETRACTED · real ~17s prep sequence identified · csdirect patched
2026-05-25 20:15-20:40 HST · Walked the only Tech2Win shim capture with a successful seed pull (2026-05-21-bench-tech2win-copctrl.log) end-to-end · Tech2Win NEVER sends 27 02 on $0241 — seeds from $27 01 knock are discarded · Two prior analyses (j2534_pro.bin firmware-blob theory + FUN_005C3576 UDS-orchestrator theory) pushed back on with receipts · Memory cleaned up · csdirect's 10s settle dropped to 500ms
Now
Day arc (evening session): Chris forwarded an analysis claiming j2534_pro.bin was "the firmware blob with the magic sequence." Verified locally — entropy 7.997 bits/byte (uniform random, encrypted), update_j2534_pro.bat literally runs binupdate.exe j2534_pro.bin — it's an encrypted adapter-microcontroller firmware image, not a protocol blob the driver loads. Three swappable adapter personalities: j2534_pro.bin / canhacker_pro.bin / kline_pro.bin. Pushed back, then dug into where the real seam lives.

1 · FUN_005C3576 reread ⚠️ Pulled the existing 777-line decompile at saab_security_project/new_decompile/FUN_005C3576_decompile.txt. Function is a UI list-picker, not a UDS pre-prep orchestrator. Evidence: param_1+0x16 is item count, param_1+0x5a is a 8-byte-stride {key,value} array, lines 73-123 are bubble-sort dedup of that array, param_1+0x70 is a selection cursor index bumped by arrow events (sVar10 == 4/6), ENTER (sVar10 == 5) commits selection. The 45 EXEC dispatches push VM message IDs for rendering text/fields to the scenario-VM dispatcher at 0x1E40A0, not CAN frames. The "10 pre-UDS EXEC(op=4) steps from FUN_005C3576" framing from the prior agent thread was over-extrapolated.

2 · Golden shim log end-to-end walk ✅ Per [[reference-shim-log-corpus-index]], the only instrumented Tech2Win capture with a complete 27 0B → 67 0B C4 DC seed pull is Chipsoft_RE/shim/cstech2win/captures/2026-05-21-bench-tech2win-copctrl.log. Walked all 49 REQ-PDU events. The actual ~17s pre-unlock sequence on the bus:
  1. Functional TesterPresent priming on CLL=5 — $0101 FE 01 3E ×3
  2. Gateway capability handshake — ComParam 0x000D001F set to ... 01 01 FE 1A 9A, then $0101 FE 1A 9A functional probe → gateway @ $7E9 replies 5A 9A 01 01 (gateway capability ack)
  3. 1A 90 VIN walk on multiple modules: $45, $42, $41, $43, $46, ... — each returns 5A 90 + ASCII VIN (bench VIN YS3FD49YX41012017)
  4. $27 01 Level-01 knock on 8 SWCAN modules + ECM functional: order $57, $42, $41, $43, $46, $48, $4A, $4B then $07E1. Each returns its L01 seed; every seed is discarded — no 27 02 key send anywhere in the log.
  5. ComParam keepalive switch FE 1A 9AFE 3E 01 (standard TesterPresent on $0101)
  6. AA 01 01 cadence on $0241 at ~440ms, 4 cycles; each returns NRC 7F AA 78 pending then a 12-byte status payload 41 01 NN 06 02 02 FD 04 FC with incrementing counter (NN: 66 → 65 → 68 → 69)
  7. AE 00EE 00 mode-shift ack
  8. 1A 3F (fired exactly ONCE, not as a cadence) → 7F 1A 78 pending → 5A 3F <VIN ASCII>
  9. 27 0B7F 27 78 pending → 67 0B C4 DC
3 · Two prior-agent claims corrected ⚠️
  • "Active-poll cadence for 1A 3F" was wrong. 1A 3F fires exactly once, immediately before 27 0B. No repeated 1A 3F chatter anywhere in the corpus.
  • "Missing 27 02 step" hypothesis RETRACTED. Tech2Win uses $27 01 purely as a presence probe with seed discarded — it never sends 27 02 to $0241. The bench observation that 27 02 00 00 returns NRC 0x35 (invalidKey) is genuine but irrelevant — Tech2Win doesn't enter that state. Memory [[project-saab-27-02-missing-step]] updated with retraction; algo-candidate research preserved for eventual level-0B (27 0C) key work.
4 · COP-CTRL physical/functional discriminator identified ✅ COP-CTRL byte 0x0C: 00 for physical sends on $0241, 01 for functional sends on $0101. pydpdu must set this correctly per send.

5 · Memory + code updates ✅
  • Wrote [[project-saab-prep-sequence-before-27-0b]] with the byte-exact recipe
  • Updated [[project-saab-27-02-missing-step]] with retraction
  • Updated MEMORY.md Android-J2534 project anchor
  • Wrote memory/2026-05-25.md daily log
  • Patched NoMoreGlobalPy/csdirect.py: dropped SETTLE_SECONDS from 10.0 → 0.5 (the 10s wait was justified by the now-retracted "27 02 times out" theory; golden log actually shows ~360ms between last 27 01 and first AA)
  • Added TODO comment on SWCAN_MODULE_WALK_27_01 flagging conflicting evidence: 5/21 shim log says $57 first, 5/24 USBPcap analysis in existing code says $41 first — to try alt order if cold-start still fails
What I deliberately did NOT change:
  • Module walk order — 5/21 shim and 5/24 USBPcap analyses disagree. Need a fresh side-by-side to settle.
  • AA count (currently 7) — 5/21 shim shows 4 in the cluster (+1 isolated 12s earlier); 5/24 USBPcap analysis in existing code says 7. Could be a layer-difference (D-PDU vs USB byte) or two genuinely different runs.
Open items:
  • The golden log predates commit a100642 (EXP-ENTRY walker) — the 64-entry pExpectedResponseArray contents still aren't dumped. One fresh Tech2Win run on .59 with the current shim closes that gap. Still required for pydpdu to mirror Tech2Win at the D-PDU layer.
  • Map CAN IDs $42 $43 $46 $48 $4A $4B $57 → physical module names via z90.pl / OpenSAAB catalogs.
Next ▶ Bench session: run patched csdirect. If 1A 3F still draws 7F 1A 22, flip the module-walk order to [$57, $42, $41, $43, $46, $48, $4A, $4B] and rerun. If that works, the order theory is confirmed.
⚠️ 2026-05-25 — csdirect "seed without Tech2Win" RETRACTED · Tech2Win-primed false positive · still real wire-layer gains
2026-05-25 HST · Cold-start csdirect fails 7F 1A 22 on bench where Tech2Win pulls 67 0B C4 DC cleanly · 5/24 success runs were Tech2Win-primed (~30-60s residual SAS state) · gap below USBPcap visibility · 4 wire-fidelity fixes still merged + verified vs Tech2Win byte-for-byte
Retracted
⚠️ RETRACTED 2026-05-25 09:40 HST: the "without Tech2Win" claim was a false positive. Cold-start csdirect today consistently fails with 7F 1A 22 (conditionsNotCorrect) on 1A 3F. Today's USBPcap-on-Tech2Win against the same bench works fine (67 0B C4 DC). The two "successful" csdirect runs on 5/24-5/25 night were Tech2Win-primed — we'd been driving Tech2Win for hours and the SAS module retained state for ~30-60 sec post-unlock. My SSH-run that got C4DC at 12:24 today was also within 60 sec of a Tech2Win pull. Same priming effect.

Also wrong: the "filter_refresh_sas before 1A 3F is the missing primitive" claim. USBPcap diff today proved Tech2Win does 0 TAG24 / 0 SET_FILTER in the AE 00 → 27 0B window. Removing the harmful refresh calls (commit 91577d1) didn't unblock cold runs either — bench still 7F 1A 22.

RE target correction: yesterday I pointed at tech2.dll.bin as the OS-layer RE target. That's a GlobalTIS security DLL, not Tech2Win's wire layer. Tech2Win's actual wire stack is Tech2Win.exe → CSTech2Win.dll → adapter firmware. If RE remains the path, the target is CSTech2Win.dll (~130 KB), not tech2.dll.bin.

What still holds (USBPcap-verified vs Tech2Win byte-for-byte):
  • TAG13 (CMD_RX_BUFFER_CLR, 0x0D) RX-buffer barrier before first CONNECT
  • Per-channel CONNECT → TAG24 → SET_FILTER interleaving at session start
  • TAG24 SWCAN proto-select between SET_PARAM and SET_FILTER (init only)
  • BLOCK filter encoding with inner_proto=FUNC (0x8008 SWCAN, 0x0005 HSCAN)
Merged in csdirect.py @ 91577d1 and chipsoft-android @ 9857456. The filter_refresh_sas helper exists but is no longer called.

Honest current state: csdirect's wire bytes match Tech2Win at every USBPcap-visible layer; SAS module still rejects csdirect cold. Gap is below the USB/API byte boundary — either CSTech2Win.dll's internal state or adapter firmware. Production path remains Tech2Win + cstech2win shim + seed_pipeline.py.

Original (now-retracted) claim, preserved as record:

Day arc: Ghidra dive on NAO.bin → USBPcap diff csdirect vs Tech2Win golden → five wire-layer fixes layered onto the prior AE 00 preamble → bench-validated seed pull twice without Tech2Win in the loop. Closes the near-term goal from [[project-saab-seed-to-bojer-pipeline-goal]].

1 · NAO.bin architecture clarified ✅ PyGhidra dive on the Saab NAO.bin (32 MB M68K big-endian) decoded FUN_005C5628 as a 5-level SAS state machine (0xB → 0xC → 0xD → 0xE → 0xF), each step pushing (level, opcode=4, mode=1) to 0x1E40A0 scenario_step_EXEC. Critically: dispatcher trampolines at 0x1E40A0 / 0x1E1DB0 / 0x1E1E10 dump as FF FF in NAO.bin — they're populated at boot by the Tech2 OS layer in saab_security_project/new_decompile/data/god_mode_dump/tech2.dll.bin (929 KB stripped x86 PE32 DLL). Implies pure-RE-without-Tech2Win was always going to require either that DLL or a wire-layer workaround. Took the wire-layer path.

2 · USBPcap structural diff vs Tech2Win golden ✅ Decoded tech2win_SUCCESS_670BC4DC_20260524-122240.pcap (1.66 MB, full unlock) and a fresh csdirect capture. Op-count diff exposed the gaps: SET_FILTER 68 vs 996 (15.5×), TAG24 2 vs 84 (42×), READ_MSG 805 vs 4444 (5.5×). Tech2Win runs a continuous TAG24 → 1-2 SET_FILTERs cycle every ~5 USB ops throughout the unlock dance; csdirect installed filters once and never refreshed.

3 · Five wire-layer fixes committed to NoMoreGlobalPy/csdirect.py
  • TAG13 (CMD_RX_BUFFER_CLR, opcode 0x0D) barrier before the first CONNECT
  • Per-channel CONNECT → TAG24 → SET_FILTER interleaving — Tech2Win builds each protocol stack to completion before the next; csdirect used to parallelize both
  • TAG24 (CMD_PROTO_SELECT, opcode 24) BETWEEN SET_PARAM and the first SET_FILTER per channel. SWCAN payload 07 80 00 00 (LE u32 = 0x00008007); HSCAN 06 00 00 00 (= 0x0006). Earlier TAG24 attempts failed because the code inserted it AFTER filters — wiping them. Ordering was the bug.
  • BLOCK filter encoding with inner_protocol arg on build_set_filter. Tech2Win uses outer=PHY (0x8007) but inner=FUNC (0x8008) on the 21-byte record's proto field. Same pattern on HSCAN. Resolved 3 weeks of ERR_0x6E on every BLOCK install attempt.
  • filter_refresh_sas — the missing primitive. One TAG24 + one FLOW_CONTROL SET_FILTER refresh of the $0641 channel, fired right before each 1A 3F retry AND right before the 27 0B seed request. Adding BLOCK to the refresh ate the receive path (0 RX lines); pure FLOW_CONTROL is what works. Hypothesis: the SAS module's "is this Tech2" check reads filter freshness.

4 · Bench validation ✅ Two consecutive runs against the bench ECM on .80 (YS3FD49YX41012017, COM7, Chipsoft J2534 Pro): RX 00 00 06 41 ee 00 (gate flipped) → filter_refresh_sas00 00 06 41 7f 1a 78 (responsePending — first time csdirect ever saw this) → 00 00 06 41 5a 3f <VIN ASCII> (SAS module READY) → filter_refresh_sas00 00 06 41 7f 27 7800 00 06 41 67 0b c4 dc (seed 0xC4DC). Reproducible.

Commit: 77d685d NoMoreGlobalPy: csdirect.py pulls SAS seed without Tech2Win on branch nomoreglobalpy-dpdu-2026-05-21 (origin synced).

Memory anchors: [[project-saab-seed-without-tech2win-achieved]], [[project-saab-nao-sas-vm-layout]] (extended with tech2.dll.bin pointer + FUN_005C5628 decoded state machine).

Next ▶ port the five fixes into chipsoft-android (Kotlin) to close out the Android-direct seed pull. Then end-to-end: csdirect seed → security_calc → Bojer POST → unlock binary written back to ECM with 04 27 0C 4E ED on $0241. Begin FastAPI emulator scaffold using NAO.bin FUN_005C5628 state machine + tonight's wire-side findings as the workflow spec.
2026-05-22 — Tech2Win preamble decoded · j2534_interface.dll byte-patched · shim auto-arms logging · csdirect AE 00 added
2026-05-22 HST · 10-byte static patch in OEM DLL forces Boost.Log ON · shim bootstrap module arms options.json · AA 01 01 → AE 00 → 1A 3F preamble extracted from fresh shim capture · Collector tray "Open log folder" bug fixed (now distinguishes driver vs shim)
Now
Day arc: chased Tech2Win's pre-1A 3F session preamble end-to-end. Found that the bytes csdirect.py was missing are AA 01 01 ×3 → AE 00 → EE 00 on $0241. Along the way, hardened the Chipsoft logging path so this kind of capture will never silently fail again.

1 · Byte-level patch of j2534_interface.dll Ghidra showed the Boost.Log sink in FUN_10011a99 is gated by a single byte at [edi+0xc] (LogLevel after boost::ptree). Built Chipsoft_RE/tools/patch_j2534_interface.py that NOPs the jae at RVA 0x11A9D (6 bytes) and zeroes the severity read at RVA 0x11B3D (4 bytes). 10 bytes total, length-preserving, idempotent. Validates pre-image bytes (refuses if Chipsoft updates the DLL). Applied to the installed DLL at C:\Program Files (x86)\CHIPSOFT_J2534_Pro_Driver\; original safely at .original.bak. Driver now physically cannot skip the sink — for J2534-side clients.

2 · Critical RE finding — Tech2Win does NOT use Boost.Log ⚠️ Strings on CSTech2Win.dll show zero references to options.json, LogLevel, ProgramData, logs\, or j2534_interface. Tech2Win → CSTech2Win.dll never touches j2534_interface.dll. The byte patch helps TrionicCANFlasher / pyj2534, not Tech2Win. For Tech2Win sessions, the cstech2win shim is the only byte capture path.

3 · Shim bootstrap module ✅ Added src/bootstrap_chipsoft_config.c: in DllMain(DLL_PROCESS_ATTACH), before the real DLL loads, ensures %ALLUSERSPROFILE%\CHIPSOFT_J2534\ exists; reads options.json, backs it up + rewrites with LogLevel: 0 if missing/invalid/≥5; creates + smoke-tests the logs\ dir. On DLL_PROCESS_DETACH: writes the newest CS log filename as an EXIT |newest CS log: breadcrumb. Defence-in-depth alongside the byte patch. Shim rebuilt (131 KB), deployed at the install path; backup renamed to CSTech2Win_real.dll.

4 · The AE 00 preamble ✅ Pulled a fresh Tech2Win shim capture, found the bytes between AA 01 01 and 1A 3F: AE 00 (DeviceControl, sub 0) → EE 00 positive. Added send_ae00_preamble() to NoMoreGlobalPy/csdirect.py, wired between keepalive_both and warmup_1a3f. csdirect.bat launcher created (matches vin.bat convention). pyserial 3.5 installed in 32-bit Python 3.13 on the EliteBook.

5 · OpenSAAB-Collector tray log-folder fix ✅ The tray's "Open log folder" was hardcoded to %ALLUSERSPROFILE%\CHIPSOFT_J2534\logs — empty for Tech2Win-driven sessions, which had us thinking the shim was broken when it was actually writing to %TEMP% all along. Renamed to "Open Chipsoft driver log folder" + added "Open shim log folder (Tech2Win)" that highlights the newest cstech2win_shim_*.log. Rebuilt + deployed (153 MB self-contained); tray restarted.

Bench state at stopping point: Tech2Win itself stopped completing the AE 00 → 1A 3F sequence — its AA 01 01 retries returned 7F AA 78 (responsePending) without committing. csdirect failed identically, which confirms it's NOT a code regression — bench needed a power-cycle which we deferred. Resume tomorrow on a freshly-cycled car.

Next ▶ ignition cycle → one Tech2Win run to confirm bench is back to the 2026-05-21 known-good state → re-run csdirect.bat with the new preamble → seed should land. If yes, immediately pipe through to Bojer for the 714B post-auth. If still no, capture the fresh Tech2Win shim log and diff against ours byte-by-byte.
2026-05-21 — D-PDU seed pull: 3 blockers cracked via Ghidra · transport hang killed · pydpdu runs end-to-end
2026-05-21 HST · PDUCreateComLogicalLink + ComParams + SENDRECV-hang all fixed · full D-PDU sequence runs clean · one gap left: no ECU replies
Now
Day arc: drove NoMoreGlobalPy/pydpdu.py from "PDUConstruct fails" to a clean end-to-end D-PDU run on the bench. Three separate blockers cracked — two by Ghidra-decompiling CSTech2Win.dll, one by re-instrumenting the shim. The transport hang that stonewalled every run is dead.

Blocker 1 — PDUConstruct ✅ Last session's FCT_FAILED was simply the Chipsoft adapter not being plugged in. With the bench hot, the full D-PDU lifecycle (Construct → GetModuleIds → ModuleConnect → GetVersion → Destruct) runs green.

Blocker 2 — PDUCreateComLogicalLink FCT_FAILED ✅ Ghidra-decompiled the CLL-setup core FUN_1000d0f0: its 3rd instruction requires pCllCreateFlag.NumFlagBytes == 4. pydpdu passed 0 → internal code 0x10020FCT_FAILED. The shim had never logged that argument, so the earlier "byte-identical to Tech2Win" diff missed it. Fixed — the CLL now creates.

Blocker 3 — ComParams ✅ pydpdu was never configuring the link. Decoded all 8 per-CLL ComParams from the 2026-05-21 shim capture (0x000D002B = 33333 baud + 7 timing params), bound PDUSetComParam + the PDU_PARAM_ITEM struct, wired them in. All accepted NOERROR.

Blocker 4 — the SENDRECV hang ✅ Every UDS send blocked forever inside CSTech2Win's transport layer. Re-instrumented the shim to hexdump the one argument never captured — PDU_COP_CTRL_DATA. Tech2Win's $27 0B CoP passes NumReceiveCycles=0; pydpdu passed 1 — one receive cycle too many, so the transport waited forever on a frame that never arrives. Fixed (NumReceiveCycles=0, Time=500, 4-byte TxFlag). The hang is gone — pydpdu now runs the entire 7-request sequence + teardown clean, every PDUStartComPrimitive returning NOERROR.

Remaining gap — no ECU replies. pydpdu runs end-to-end but every request reads "no reply", even a basic $AA 01 01. The shim log shows each SENDRECV produces only 0x1301 STATUS events, never 0x1300 RESULT events — the CoP completes but collects no response. Tech2Win on the same bench gets RESULT events with the UDS bytes.

Next ▶ one cheap test first, then a build: (1) the last pydpdu runs may have been on a cooling bench — a confirmed-hot run decides whether replies were ever possible; (2) if still silent — pydpdu creates one CLL ($0241), but Tech2Win's golden recipe creates six and runs a sustained fe 3e keepalive on the functional $0101 channel, which the $27 0B SAS path requires. Add the functional channel + keepalive to pydpdu.

Ops: watchdog added (seed_watchdog.ps1 — 60 s hard timeout, kills python + frees COM5 so a hang can't black-screen the bench). Shim COP-CTRL + CreateComLogicalLink instrumentation committed + pushed to Chipsoft_RE (ddd1f03). OpenSAAB-Collector installer rebuilt — opensaab-collector-setup-0.3.0.exe, bundling the new shim. Bench car battery died mid-session — on the charger.
2026-05-20 — J2534 path closed (7 dead ends) · pivot to D-PDU · pydpdu.py wrapper built
2026-05-20 HST · sustained-keepalive hypothesis falsified on a clean run · raw-CAN pin-1 test failed at preflight · full ISO 22900-2 D-PDU ctypes wrapper built · PDUConstruct blocker characterised
Now
Day arc: drove the SWCAN $27 0B SAS seed read down the last J2534 avenues to a definitive close, then pivoted to the D-PDU API and built a complete Python wrapper for it.

J2534 — closed for good ✅
Sustained-keepalive hypothesis built & falsified. Frame-by-frame analysis of the one winning Tech2Win capture (cstech2win_shim_20260507-014723) found Tech2Win runs a sustained functional TesterPresent (FE 3E) on 0x0101 the whole session. Rebuilt seedkey_read.py + pyj2534.py to replicate it. Clean stable bench run: $1A 3F polled 26× over 25 s with the keepalive firing continuously — all 7F 1A 22. Falsified.
Raw single-wire path ruled out. Chipsoft's own J2534 compare-table confirms the Pro hardware supports raw CAN + SWCAN + a CAN bus on pin 1 — but the DLL exposes no raw single-wire protocol: 0x8000ERR_PROTOCOL_NOT_SUPPORTED, 5 routes to the HS-CAN pins, 0x8007 is ISO-TP only. The protocol-5 + J1962_PINS=0x0101 test (seedkey_pin1.py) was accepted but the transceiver never switched — bus preflight drew ERR_CAN_WRITE_TIMEOUT.
Verdict: six distinct J2534 theories plus the pin-1 test, all dead with the same 7F 1A 22 / 7F 27 22. The Chipsoft J2534 DLL structurally cannot reach the state $27 0B needs — only Tech2Win → CSTech2Win.dll (D-PDU) has ever produced 67 0B C4 DC.

D-PDU pivot — wrapper built ✅
NoMoreGlobalPy/pydpdu.py — a complete ctypes wrapper over CSTech2Win.dll's D-PDU (ISO 22900-2 / MVCI) API: all 33 exports bound with argtypes, every pdu_api.h struct mirrored (PDU_COP_CTRL_DATA, PDU_EVENT_ITEM, PDU_RESULT_DATA, …), an event callback + event pump, high-level helpers, and a request_seed() replay skeleton. Transcribed from the Softing/AUTOSAR reference headers in external/dpdu-passthru/.
Smoke test on the EliteBook: 32-bit Python loads the DLL and all 12 needed D-PDU exports resolve — then PDUConstruct → PDU_ERR_FCT_FAILED. Tech2Win's PDUConstruct succeeds with identical args, so the failure is host-process-environment dependent. Ruled out tonight: argument values, working directory, COM init, shim-vs-real DLL.

Next ▶ evidence-based debug of PDUConstruct — Ghidra-decompile it in CSTech2Win_real.dll to find the FCT_FAILED branch and/or Procmon a live Tech2Win construct — then wire request_seed() and replay the winning sequence.

Ops: Koyeb redeployed (recurring free-tier deep-sleep instability — fifth redeploy today; the real fix is a min-instance-1 / paid tier).
2026-05-19 — Android VIN read WORKING ✅ · raw-CAN→ISO15765 root cause solved · NoMoreGlobalPy built
2026-05-19 HST · Windows + Android both read the bench VIN · 3 wire-format bugs fixed from a full-snaplen capture · OpenSAAB YAML + 2 lessons
Now
The headline: the chipsoft-android app read the bench car's VIN directly off the bus — RX $07E8 5A 90 59 53 33 46…YS3FD49YX41012017. This closes the "Android transport silence" thread that has run through every Android card since 2026-05-09.

Day arc: went from "Android has never produced a UDS reply" to "Android VIN read working" — by first building a clean Windows reference tool, using it to isolate the root cause, then porting the verified wire format.

Wins ✅
NoMoreGlobalPy built — a Python/tkinter J2534 workbench (NMG-styled: status bar, command groupboxes, Consolas console). Drives the Chipsoft Pro via ctypes over the OEM j2534_interface.dll. Reads the bench VIN end-to-end on Windows: ISO15765 connect → flow-control filter → $1A 905A 90.
Self-recursing shim DLL caught — the install-dir j2534_interface.dll (and its _real backup) are both 126 KB logging shims that forward PassThruOpen into themselves → stack overflow. The genuine 1.1 MB OEM driver only exists in the RE corpus. Always load the OEM DLL.
Root cause of months of Android silence found: chipsoft-android connected in raw-CAN mode; it needed ISO15765 protocol mode. Raw CAN doesn't route the $07E0/$07E8 diagnostic addressing to the ECM — the app heard only body-bus noise. Same bug behind every historical "Android silent" failure, including the long $0241 chase.
ISO15765 path ported to chipsoft-android — new startFlowControlFilterIso15765, an ISO15765 write builder, and a numbered VIN button (button 7). Built, installed over wireless adb, bench-confirmed.
OpenSAAB updated — new commands/saab/vin_read_did_90.yaml ($1A 90 / $07E0 / ISO15765, including the "$22 F1 90 → NRC 0x11" finding). Committed + pushed (ad95ace).

Three wire-format bugs — found via one full-snaplen USBPcap of the verified-working Windows read (vin_iso_hub1.pcap):
  • Connect flags — Android used CAN_ID_BOTH (0x800); the working path uses 0.
  • Flow-control filter — real struct is 71 bytes (3×21-byte slots, CAN-ID at slot start); the earlier 75-byte version (built from a truncated Tech2Win capture) put the IDs in the wrong place, so the ECM's reply was filtered out before the app ever saw it.
  • Write header — real layout is [Timeout][ChannelID][msgId][(TxFlags<<16)|DataSize][Timestamp]; the code had TxFlags at offset 0. Also: single WriteCommit, no Queue/SetConfig/ArmChannel.
Lessons 📚 (LESSONS.md L016, L017):
L016 — capture ground truth at full fidelity before porting a binary protocol; a truncated (snaplen-96) or wrong-source capture is worse than none.
L017 — a frame that ACKs and loops back is transmitted, not correct; when a clean TX draws no reply, the bug is in frame content or the RX path.

What this changes: the Android client can now read live ECU data over ISO15765 — the transport that has blocked on-device work for weeks is open. The same ISO15765 recipe applies to every other UDS service ($27, $22, $1A) on the engine bus.
2026-05-18 — CSTech2Win.dll RE breakthrough · D-PDU corpus vendored · Collector v0.2.8-alpha released
2026-05-18 HST · pdu_mdi2 rosetta · shim struct fix · engine vs SAS path clarified · CIM-gateway topology documented · 5 memory entries written
Now
Day arc: deep-research day across four prior-art sweeps (gocan + CaringCaribou, Ukrainian Chipsoft forum, Reddit/SaabCentral practitioner threads, D-PDU/Tech2Win RE source hunt). Yielded one critical shim bug fix, a vendored 4-repo D-PDU reference corpus, and two strategic clarifications that redirect the bench work.

Wins ✅
CSTech2Win.dll RE breakthrough via tradingalerts607/pdu_mdi2 — a working open-source ISO 22900-2 D-PDU shim with identical architecture (Tech2Win → PDUStartComPrimitive → PassThruWriteMsgs → adapter → ECU, just VPW not GMLAN-CAN). Contains full #define tables for the 0x1200/0x800x PDU constants, all IOCTL IDs 1–18, complete PDU_PARAM_ITEM struct layout, and Source.def listing the 29 PDU* exports Tech2Win demands. Closes the "no public CSTech2Win RE prior art" gap definitively — Chris's RE corpus is no longer alone.
Shim struct bug fixed — cross-reference revealed our T_PDU_PARAM_MIN in Chipsoft_RE/shim/cstech2win/src/wrappers.c:34-39 was 4 fields / 16 bytes; canonical is 5 fields / 20 bytes with leading ItemType. Every PDUSetComParam log line ever captured was off by one slot — the repeating ComParamId=0x00001200 was actually ItemType=PDU_IT_PARAM (struct marker, not a parameter id). Fix landed in djfremen/Chipsoft_RE@dbd807d; new shim built via MSVC on EliteBook; deployed live. Next Tech2Win capture will be the first readable record of the ~60-call channel-init burst.
OpenSAAB Collector v0.2.8-alpha released — bundles the struct-fixed shim. Critical upgrade for anyone on v0.2.5-alpha (released 2026-05-16, has the broken-struct shim → uploading mis-aligned ComParam data). Release page with full notes + uninstall→reinstall guidance.
Engine ECU vs SAS bench-card SecurityAccess clarified via mattiasclaesson/Trionic (shipping C# stack, thousands of SAAB users). The Bosch ME9.6 engine ECU SecurityAccess lives at $07E0/$07E8 level $FD (key level $FE), uses the MOTRONIC96 4-step VM recipe — same opcodes our SecurityCalculator.kt already implements (23/23 Bojer parity). The $0241/$27 0B path we'd been chasing for weeks is the separate SAS/IMMO bench-card system, not the engine ECU. Per-ECU SecurityAccess table (T7/T8/ME9.6/Z22SE Main/MCP) documented in memory + dashboard.
NG 9-3 CIM-gateway topology nailed down via SaabCentral thread 759182 + Chipsoft Russian PDF + Ukrainian forum agent. Chipsoft Pro has 4 firmware modes (J2534/K-Line/CANHacker/Tech2Win) selectable via "CHIPSOFT J2534 PRO - Make J2534" tool, and 5 pin-pair routings (J1962_PINS values 0x0606/0x0C0D/0x0109/0x0308/0x0101) selectable via IOCTL within J2534 mode. Standard NG 9-3 = Pin 1 SWCAN 33.3k → CIM gateway → all internal modules. Resolves the long-running "are we even talking HSCAN" question.
D-PDU prior art corpus vendored under saab_security_project/external/: pdu_mdi2 (working VPW shim), DiagProf/ISO22900.II (2.7 MB C# wrapper with the ISO_15765_4.cs ComParam name table — every CP_CanPhysReqId/CP_CanFillerByte/CP_StMin/CP_Bs/... typed), AUTOSAR interface.h (official PDU_ERR_EVT_* codes), witoldo7/dpdu-passthru fork (cleanest cop-queue template). Plus CaringCaribou. Survey report.
Android chipsoft-tech2 client hardened — GMLAN TP form fix ($0101: FE 01 3E), opt-in DLC=8 padding, anchored hasUdsReply (no more DPID-flood false positives), extractNrc + awaitUdsResponse with ResponsePending handling up to 25 s. New buttons staged: button 3 "Engine SecAccess ME9.6 ($07E0/FD Trionic path)", button 4 "VIN via SWCAN/Pin-1 → CIM gateway (33.3k)". VIN ISO-TP FC fix landed — discovered ECM was replying with proper First Frame all along (07 E8 10 13 5A 90 YS3F); we just never sent 30 00 00 flow control to ask for the consecutive frames. Plus the VIN regex was bugged (required 19 chars, SAAB VINs are 17). All staged locally; awaiting phone re-pair to push.

Course corrections ⚠️
  • "GMLAN TP form was the regression" hypothesis was wrong. Reverted from new GMLAN form back to legacy OBD-II form, re-ran the test — ECM still "silent." Then discovered the ECM was never silent; we were dropping its multi-frame VIN reply because we don't send flow control. Empirical evidence beat my premature theory.
  • Memory entry project_saab_27_0b_seed_captured (2026-05-07) conflated SAS bench-card SecurityAccess ($0241/$0B) with engine ECU SecurityAccess. Both use $27 but they're completely independent subsystems. Memory now corrected; new entry project_saab_engine_vs_sas_securityaccess documents the full per-ECU table.
  • Memory entry project_chipsoft_loglevel1_mothballed had wrong LogLevel value — claimed enable was 1; manufacturer's pinned forum post says enable is 0, default-disable is 10. Corrected.
Open follow-ups 🔁
Bench-test the staged Android buttons (1/3/4) — waiting on phone re-pair.
Fresh Tech2Win capture with the struct-fixed shim — install v0.2.8-alpha (uninstall v0.2.5 first), run any Saab nav, pull log, decode the now-readable PDUSetComParam burst, cross-reference Chipsoft's 0x000D00xx vendor namespace against ISO22900.II ComParam names.
FremSoft MVP scope — pdu_mdi2 ships 38 PDU* exports; Tech2Win only actually called 7 of them in the 2026-05-16 bench capture. That's the real MVP envelope for FremSoft replay/standalone mode.
Try removing the $10 02 preamble — Tech2Win shim re-audit shows it goes straight to $AA 01 01 (DPID setup) → $1A 90$27 01, never sends $10 02. Our defensive $10 02 step is harmless fall-through (~3s wasted on no-reply) but the real Tech2Win unlock path is via $AA 01 01 which we don't yet send.

What this changes for the iOS app: the dashboard JSON shadow (/api/dashboard.json) now exposes the engine-vs-SAS distinction, the v0.2.8 release link, and the CIM-gateway topology. iOS clients can branch UI on engine-side vs body-side workflows accurately. The Trionic path ($07E0/$FD + MOTRONIC96 recipe) is end-to-end implementable in iOS without bench dependence — same algorithm we already ship.
2026-05-13 — Open community pivot · openSAAB.com landing live · Collector v0.1.0-alpha shipped
2026-05-13 HST · 5 OpenSAAB YAMLs · 35 DTCs decoded · Collector installer published as GitHub Release · landing page live on Koyeb
Now
Day arc: went from "Chris alone at the bench" to "anyone in the SAAB community can run a Windows installer that contributes anonymized shim logs to a central catalog." Strategic pivot mid-day from bench-only RE to community data collection. End-to-end pipeline shipped: capture → decode → catalog → publish, with the openSAAB.com public face + a downloadable Collector to scale beyond a single bench.

Wins ✅
Four new Tech2Win menu actions catalogued in OpenSAAB from morning bench captures: check_ignition_key_status (DPID 0x0B), check_codes_read_dtcs (full 17-ECU $A9 81 sweep w/ masks 0x10/0x12), engine_me96_read_codes (single-ECU OBD-II $07E0 mask 0x0C → 22 DTCs), clear_codes_clear_dtcs ($04 sweep across 20 ECUs). Plus the morning's check_ignition_key_status. All with z90.pl-sourced English DTC descriptions.
Trionic-cited DTC decoder at OpenSAAB/tools/decode_gmw3110_dtc.py. Caught that my initial DTC name decoding was wrong (e.g. C1 22 00 is U0122-00 "ESP Missing on BUS", not C1122-00). All 35 captured DTCs now correctly named.
SAAB ECU address map published: OpenSAAB/docs/saab_ecu_address_map_2026-05-13.md. 11 CAN-IDs identified by physical module via z90 attribution + Trionic constants: $0241 engine ECM (Tech2Win alias), $0242 BCM, $0243/$0248/$024A/$024B four door modules (sharing B3832 anti-pinch DTC), $0245 ACC/heated-seat, $0249 TCS/ESP, $024F UEC (front lighting), $07E0 ME9.6 OBD-II. Confidence levels noted; ambiguities (Trionic CIM-vs-ACC at $0245; door slot disambiguation) flagged as open.
j2534_interface.dll shim built end-to-end on EliteBook over SSH. Mirrors the cstech2win shim infra. 6 instrumented PassThru* wrappers (Open/Connect/Ioctl/StartMsgFilter/WriteMsgs/ReadMsgs); 8 naked-jmp forwarders. Hardware timestamp capture confirmed available via PASSTHRU_MSG.Timestamp. Both shims now ready for the same dogfooding round.
USBPcap decoder ships in Chipsoft_RE: tools/decode_chipsoft_pcap.py + tools/usbpcap_chipsoft_envelope.py. Used to fully decode the 43 MB TrionicCANFlasher Read ECM pcap → identified two missing OpenSAAB workflows: $A5 ProgrammingMode entry + $34 RequestDownload + ISO-TP CF-stream loader upload (5500+ frames). Captured for later catalog work.
openSAAB.com landing page live (currently on Koyeb auto-domain pending DNS re-route). Mission statement, three CTAs (browse repo / download Collector / how to help), live community-capture counter polled from /api/health, credits, Forums tab → GitHub Discussions, "No More Global ↗" tab → Chris's YouTube. Old VIN-decrypt tool preserved at /lookup.
Phase 0 ingestion endpoint live: POST /ingest/shim-log on saab-security-api accepts gzipped multipart with X-Install-ID / X-Capture-Source / X-Consent-Version headers + optional vehicle profile. 50 MB hard cap. Persists to uploads/<install-id>/<source>_<wall_ms>.log.gz with sidecar JSON. Smoke-tested green in production.
OpenSAAB-Collector v0.1.0-alpha installer published as a GitHub Release (~68 MB .exe). Built end-to-end on EliteBook this evening: .NET 8 Service (BackgroundService + FileSystemWatcher + 30s settle + gzip + 5-attempt POST retry), .NET 8 WinForms tray, InnoSetup .iss with Chipsoft pre-check + consent task + DLL backup-to-*_real.dll + service install/uninstall that restores genuine DLLs byte-for-byte. Wired into landing-page download CTAs.
GitHub Discussions live as forums on djfremen/OpenSAAB. Default 6 categories. Welcome post seeded in Announcements explaining where to post what.

Course corrections ⚠️
  • Initial commit landed in wrong repo + wrong format. First check_ignition_key_status went into Chipsoft_RE as JSON. Chris flagged: should be OpenSAAB as YAML following the established commands/saab/<action>.yaml shape. Fixed; cleanup commit dropped the stray Chipsoft_RE subdir.
  • DTC name decoding was wrong on the first pass. I used "high nibble = letter" intuition; Trionic's GetDtcDescription uses bits 7-6 of byte 1 instead. Re-decoded everything against the canonical algorithm; all YAMLs corrected.
  • Bojer's real name and a private editorial-feedback doc were on the public landing after the first deploy. Chris flagged. Removed both — credit reads only "Bojer" linking sas.mysaab.info; docs/feedback_bojer_2026-05-12.md deleted from OpenSAAB entirely.
  • Tray .csproj referenced a missing app.ico; build failed first time on EliteBook. Removed the reference (runtime falls back to SystemIcons.Information); patched + re-built clean.
  • iscc.exe wasn't on PATH after winget-installed Inno Setup (per-user install). Patched build-installer.ps1 to look in $LOCALAPPDATA\Programs\Inno Setup 6\ too.
Open follow-ups 🔁
Bench validation of v0.1.0-alpha Collector on Chris's second laptop: install → consent → Tech2Win menu click → log appears under uploads/<install-id>/community_captures increments.
Add Key flow — Chris's original morning ask. Never actually fired; he ran Check Ignition Key Status by accident as preflight. Still pending.
DPID 0x0B bit layout unknown — needs no-key vs valid-key diff at the bench.
$0245 ACC vs CIM ambiguity — Trionic's FilterIdCIM says CIM at $0245 but bench DTCs there are heated-seat. Resolve via $1A 9A tape-head signature.
Door module disambiguation — $0243/$0248/$024A/$024B all return B3832-45 Anti Pinch Not Learned. Which CAN-ID is which physical door?
j2534 shim live test — built and ready; pending TrionicCANFlasher run on the bench so we see what API-level PASSTHRU_MSG.Timestamp adds vs the USB-pcap view.
DNS re-route of openSAAB.com to the Koyeb domain — Chris-owned action. Until then the Collector's hard-coded https://openSAAB.com/ingest/shim-log resolves to the wrong place.
EV code-signing cert for the Collector installer. Currently SmartScreen warns; OK for invited alpha, blocker for public release.

Today's commit landmarks:
Chipsoft_RE @ 185a2ef — j2534 shim scaffolding + USBPcap decoders + 4 fresh capture logs
OpenSAAB @ 2d8305a — 5 new YAMLs + DTC decoder + ECU map; private Bojer feedback doc deleted
saab-security-api @ 49ea2de — landing page, /lookup, /forums, /ingest/shim-log; deployed to Koyeb
OpenSAAB-Collector @ 3a2fb49 — NEW repo, 17 files; v0.1.0-alpha release published with built installer attached

Recommended next concrete step: install v0.1.0-alpha on the second laptop and run one Tech2Win menu through the full pipeline. If the round-trip works end-to-end (shim log → service detects rotation → gzips → POST → server stores → /api/health increments), the Collector is bench-validated and we can start inviting other community contributors.
2026-05-11 — Workflow catalogue stood up · Tech2Win-as-API direction set · transport still silent
2026-05-11 HST · 5 workflow defs · Bojer on-device ✅ · 19/19 cross-VIN tuples · architectural pivot proposed
Now
Day arc: the algorithm and Bojer paths are fully solved on-device. The remaining gap is the chipsoft USB transport silence on the Android side — confirmed to have been silent all day (and arguably has never worked from Android via raw CAN). The right pivot is "Tech2Win-as-an-API": move the smarts server-side, treat the phone/web client as a USB pipe.

Wins ✅
Bojer roundtrip on-device, end-to-end. Bench app button 6 loads bundled pre-auth, POSTs to sas.mysaab.info/api/process, saves 714B post-auth, walks SKA tuples, prints the engine SAS key inline (NOTE:key=0x4EED → send '04 27 0C 4E ED' on $0241). Files land at YS3FD49YX41012017_POST_AUTH_bojer_*.bin.
1367 field-car validated 10/10 against EliteBook known-good. Live Bojer POST byte-for-byte matches post_auth_1367-4MAY2026_chipsoft.bin (sha256 prefix d74f8334). Engine SAS pair: seed 0x77E5 → key 0xDA1E via algo 0x0367.
Workflow catalogue stood up at Chipsoft_RE/workflows/ with parser, segmenter, validator scripts and 5 SAAB workflow definitions: module_presence_scan, vin_read, seed_sweep_l01, engine_sas_unlock, engine_ssa_writeback. Each definition.json is portable across any host that drives the bus.
Memory expanded with 4 new entries: project_saab_714b_bojer_transformation.md (exact byte-level contract: HWKID at 0x01..0x0B, sccode at 0x26..0x2D, SAS key at 0x198..0x199); project_saab_0241_only_via_cstech2win.md; project_saab_chipsoft_usb_envelope.md; reference_bojer_api_callable.md.
Cumulative cross-validation: 19/19 tuples today (9 bench + 10 field) all match security_calc.get_key_from_seed bit-for-bit against Bojer's response. Combined with prior 33/33, the SAAB algorithm is unambiguously RE'd.

Course corrections logged today ⚠️
  1. $10 02 was never sent by Tech2Win. Spec said StartDiagnosticSession before SecurityAccess. Validator on the new workflow catalogue checked the shim log — zero 10 02 occurrences. The real preamble is $AA 01 01 (DynamicallyDefineDID) on $0241. Our Android runSasSeedRequest had this wrong; reflected in updated engine_sas_unlock/definition.json.
  2. ISO15765 protocol mode rejected by chipsoft. Tried switching J2534Protocol.CAN → ISO15765 as a fix for $0241 silence. Every opcode returned 00 00 00 BD 00 00 00 — error code 0xBD = "protocol not supported in this config." Reverted. Will need additional SET_CONFIG + flow-control filter setup, deferred.
  3. "VIN reply at 10:51" was offline-mutate echo, not live ECM. Earlier this afternoon I claimed the morning log proved a live bench VIN read. Re-grepped: every VIN ASCII match across all today's logs is from offline-mutate output (button 5) or Bojer response — never from a Poll #N RX. Android-chipsoft has produced zero UDS replies all day.
  4. Synthesis ≠ live read. Bojer preserves SKA tuple seeds and computes keys; wrong seed → wrong key → ECM rejects unlock. imposter/build_pre_auth.py generates random seeds, which means its output is structurally valid but functionally useless against a real ECM. Documented in project_saab_714b_bojer_transformation.md.
  5. Auto-detect VIN button reverted. Built a "Read VIN → identify car → Bojer" one-tap flow. Gated on a live $1A 90 reply that doesn't come. Reverted to manual car-select (buttons 6 and 6b) while transport is silent. The auto-detect code path stays in place, ready to switch on once transport works.
  6. Tech2Win is an emulator, not a plain application. emulator SAAB.exe.bin (13 MB PE32) loads a SAAB NAO.BIN as guest firmware and emulates the Tech2 CPU. The PDU encoding logic we want lives in the emulator's PDU-API call sites — already captured downstream by our CSTech2Win shim. Reframed: we don't need to RE the emulator binary; we already have its output stream in the shim logs.
What's stuck 🔒
Android-chipsoft transport silence. Every TX accepted by the chipsoft (msgIDs assigned, commits succeed); every poll returns 9-15 KB of broadcast traffic; zero UDS replies on $0641, $07E8, or any other diag reply ID, regardless of VIN. Same picture with 2017 and 1367. Tech2Win on the same chipsoft → bench responds normally. So it's the J2534/raw-CAN path from Android specifically.

Architectural decision pending 🧭
Discussed and lightly scoped the Tech2Win-as-an-API pivot: server holds chipsoft protocol + UDS state machine + workflow definitions; Android/web/iOS clients become USB byte relays over websocket. ~3-5 days for a working prototype. The 5 workflow definitions written today are step 1 of that server's content. The chipsoft USB envelope is already documented (35/37 bytes, opcodes 0x04/0x12/0x17/0x22/0x0F/0x10/0x20). The security_calc algorithm is already RE'd in two languages. Bojer is callable from CLI. ~95% of what the server needs is on disk today. Open question: commit to the server build or run one more bench session first to fill the engine_sas_unlock + engine_ssa_writeback capture gaps.

Recommended next concrete step: start the Tech2Win-as-API server (Day 1 of the previously scoped 3-5 day plan). We have the chipsoft USB envelope, the algorithm, Bojer, and 5 workflow definitions on disk already. The 2 missing steps ($27 0C key-send ack, $3D SSA write-back) have well-defined wire formats per GMW3110 §8.7-8.8 — we can spec them in the workflow JSON now and validate on the bench during testing. A bench capture would be nice-to-have, not blocking. (Note: "we already have Tech2Win's output stream from the existing CSTech2Win shim" — we are NOT building a new Tech2Win-binary shim. The workflow catalogue is built from those existing 2026-05-07 shim logs.)
2026-05-09 — On-device Bojer-parity + full chain shipped, stuck on live $27 0B no-reply
2026-05-09 HST · golden-ticket recovered, 2 Kotlin algo bugs fixed, button #6 wired, ECM not replying
Now
Today's arc: went from "bench car identity uncertain, no live unlock has ever fired" to "Android-direct on-device synthetic golden-ticket validated 9/9 vs Bojer; full live→Bojer→display chain shipped; stuck at live $27 0B getting no ECM reply." 13 sequential steps in project_saab_2026-05-09_full_story.md.

Wins ✅
Bench-car VIN corrected: YS3FD49YX41012017 (the "1367-suffix" cluster of files is a different SAAB).
Golden ticket recovered: SSH'd into EliteBook, pulled 10 SSA blobs to Chipsoft_RE/shim/cstech2win/captures/2026-05-09-elitebook-pull/. Headline file ssa_region_export-09may-processed.bin contains tuple (algo=0x0367, seed=0xC4DC, key=0x4EED).
Cumulative Bojer parity at 33/33 (12 ground-truth + 11 imposter + 10 today's bench card).
Two Kotlin algorithm bugs found + fixed:
    #AopAnd/opOr were big-endian (a2_1 + (a2_0 shl 8)); should be little-endian per Python (a2_0 + (a2_1 shl 8)). Misleading "params swapped per server contract" comment was a previous incorrect "fix."
    #B — Opcode constants declared as Byte: 0x98.toByte().toInt() = -104, never matches the unsigned (table[i] and 0xFF) = 152 lookup. OP_SUB + OP_SUB_LE arms silently no-op'd. Changed all opcode constants to Int. Recorded as feedback_kotlin_byte_constants_sign_extension.md.
Bench-app on-device offline-mutate ships valid keys: button #5 produces a 714B post-auth from assets/pre_auth_2017.bin via the on-device VM, byte-identical to Bojer for all 9 shared SKA tuples including the bench unlock key 0x4EED.
Full chain wired: bench app button #6 sequences live SAS seed request → load asset → POST to Bojer (BojerApi.kt, HttpURLConnection + JSON SSA_DATA) → parse response → diff vs local → display verdict.

Current blocker ⏳ — Live ECM doesn't reply to $27 0B from the bench app. Three runs today, three null results.
• Setup steps all clean: Connect SWCAN 33k, SET_CONFIG J1962_PINS=0x0101, PassFilter promiscuous, Arm channel.
• Queue + Commit $27 0B → $0241 accepted (cleanly committed with msgID).
• 30 polls × 50ms see heavy CAN broadcast (CAN IDs 0x110/0x120/0x130/0x150/0x180 = SAAB powertrain) but ZERO $0641 traffic and ZERO $67 0B matches.
• Already added $10 02 DiagnosticSessionControl preamble + drain — didn't unstick it.
• Already tightened extractSeed0B to require ISO-TP SF prefix 04 67 0B SS SS (caught a false positive: an earlier run reported seed=0x1700, which was a frame timestamp, not a real seed).

Open hypotheses for the no-reply: wrong session level ($10 03 next?); wrong CAN ID (we use $0241 per shim capture; maybe $0240); ECM in non-receptive state; SWCAN routing wrong physically; Tech2Win uses a longer preamble than just $10 02.

Recommended next debug step (per memory project_saab_2026-05-09_full_story.md): re-run the CSTech2Win shim with Tech2Win driving an unlock cycle, capture the FULL pre-$27 0B UDS preamble to $0241, then mirror it byte-for-byte in runSasSeedRequest. Currently we are guessing.
SAAB Bench Unlock — Algo + Key Computed Locally ✅
2026-05-07 · pre-auth SSA decoded, single deterministic key — no Bojer call
Now
End-to-end Bojer-free unlock path is closed on paper. The 714-byte pre-auth SSA was pulled from the bench Tech2Win unit (VIN YS3FD49YX41012017), decoded locally, and the single deterministic SecurityAccess key was computed without any /api/process call.

Resolution chain:
• ChipsoftRE/shim/cstech2win confirmed Tech2Win sends $27 $0B to engine ECM $0241; ECM returned deterministic seed 0xC4DC across two cold runs.
decode_ssa_for_seed.py /tmp/bench_pre_auth.bin --seed 0xC4DC → tuple #09 @ offset 0x17A binds seed 0xC4DC ↔ algo 0x0367.
security_calc.get_key_from_seed(0xC4DC, 0x0367) → Alt1 table → key 0x4EED.
• Wire reply per GMW3110 §8.8: on 67 0B C4 DC, send 27 0C 4E ED.

⚠️ Imposter-card guess was wrong. The pre-resolution candidate (algo=0x0366 → key=0xB097, "most-frequent algo from imposter card") is stale — algo 0x0367 isn't in the imposter candidate list at all. Trying 0xB097 would burn the only attempt (free shots = 0) and trigger 10-s lockout. Single-shot answer is 0x4EED.

Open: live ECM exchange to confirm 0x4EED unlocks. First validation of the RE'd security_calc.py engine against a real cold-key bench seed. Held in reserve until either (a) Tech2Win's SAS-server path is restored or (b) Android-direct via Chipsoft Pro can fire it (blocked on STARTCOMM J2534 wire opcode — see Chipsoft card).

Algorithm validation: security_calc.py matches 12/12 ground-truth tuples in ~/Desktop/tis2web_logs/ground_truth.md + 23/23 fresh Bojer responses. Engine is solved; this resolution is the first end-to-end on-device application.
sas.mysaab.info Web Client Audit — 2/3 PATCHED
2026-05-05 · #3 byte-order bug bench-confirmed + fixed in all 3 clients
Now
Pulled the live web client direct from `sas.mysaab.info` — `/js/commands.js` (75 lines) + `/js/t2comm.js` (1269 lines) saved verbatim to wiki/sources/. Site loads only those two app files plus jQuery, so this is the complete client.

Canonical recipe (now ground truth):
• POST body to /api/process: {SSA_DATA: <base64-of-714-bytes>} only — no DEBUG, no HWKID, no version.
• SSA payload at seg 0x0F / addr 0x2E0000, 714 bytes, 40-byte read chunks (opcode 0x81).
• Unlock write-back: erase (fire-and-forget) → poll buf[2]==0x55 → 2-byte prime write at SSA_LOCATION → writePrepare2(SSA_LOCATION, SSA_LOCATION+2) → 40B chunks (no all-FF skip, no retry) → restart 15s timeout (propagate, don't swallow).

Three Android divergences flagged:
#1 — PATCHED ✅ Bojer POST body included extra DEBUG: 69; web client sends only SSA_DATA. Split ProcessRequest into BojerProcessRequest (only SSA_DATA) for the Bojer path; DJFremen unchanged. Build clean. Local commit 1b85749.
#2 — OPEN ⏳ writePrepare2(BASE, BASE+714) covers full data range; web client uses (BASE, BASE+2). Could be benign protocol slack or load-bearing. Bench test required.
#3 — PATCHED ✅ (2026-05-05) buildChunkReadCommand emitted [offset_low, offset_high] (LE 16-bit offset); web client uses [addr_mid, addr_lo] (BE 24-bit address). Identical for offset < 0x100; bytes 4/5 silently swap at every offset ≥ 0x100, making the device read from 0x2E000N instead of 0x2E0N00. Every chunk past byte 256 returned shifted page-0 garbage instead of real flash — destroying the SKA tuple region at 0x132. Fixed in saab_simple, SAABSecurityAccess, and NoMoreGlobal_dotnet8_v8. Bench-confirmed on Tech2: post-Chipsoft live read is now byte-identical to Chipsoft's own dump (0 differences). Full Bojer round-trip closes the loop: HWKID S000310723 stamped, IMMO XJOUQVL7, all 9 SKA keys filled.

Push status: Workspace commits pushed to branch tech2-byte-order-fix-2026-05-05 on djfremen/saab-security-access (main divergent histories — branch+PR strategy in effect). API repo dashboard commit pushed to master.

Tracker: WEB_CLIENT_DIVERGENCE_AUDIT_2026-05-04.md · Source artifacts: wiki/sources/sas.mysaab.info_*
Android Bojer-first Field Test — WORKING ✅
2026-05-03 18:04 HST · Pixel 7 + real Tech2 + chassis 1012017
Done
End-to-end flow verified on real hardware:
1. Read PRE-AUTH (714 bytes via 64-byte chunks)
2. Show VIN YS3FD49YX41012017 to user
3. Send 714 bytes JSON to Bojer API
4. Bojer returns IMMO=XJOU INFO=QVL7
5. Write back, exit-DL commits
6. Auto-verify re-read shows POST-AUTH ✓

Read bug fix: SSA_READ_CHUNK_LEN 0x28 → 0x40 to match C#. The 40-byte JS-derived chunk size returned wrong bytes past the chunk boundary. C# uses 64. Once aligned, Android sees what C# sees.

Auto-verify after write: write path now re-enters DL → reads 714 bytes → runs detector → exits DL, all without user input. UI updates with verified status, not optimistic.

Standing rule (L012): CMD_RESTART_DEVICE is forbidden. Use CMD_EXIT_DOWNLOAD_MODE only.
Android SKA Tuple Fix — DONE ✅
Fixture files replaced, offset 0x130 confirmed correct, key chain verified
Done
Root cause: Android assets had WRONG fixture files — missing or corrupted SKA data.

Fix applied:
1. Replaced pre_auth_2017.bin with verified fixture (9 SKA tuples)
2. Replaced pre_auth_1367.bin with verified fixture (9 SKA tuples)
3. Confirmed pre_auth_2996.bin was already correct (11 tuples)
4. Verified all keys match Bojer oracle via live API

Offset 0x130 is CORRECT — not 0x132. Field order [key][status][algo][seed] is correct.
Key chain: entry[N].key = vm_get_key(entry[N-1].seed, entry[N-1].algo & 0xFF, table_1)
Verified: 2996 (11/11), 2017 (9/9), 1367 (9/9) all match oracle exactly.
HWKID + Mutation Processor (A07-A08)
Extract HWKID, mirror to known blocks, build SsaMutationProcessor
Now
Build SsaMutationProcessor that copies input, injects code, applies HWKID, iterates SKA tuples, fills missing keys via NativeVM. Must match server /api/process output byte-for-byte on fixtures.

📡 SAAB Module Map · bench reference (2026-05-15)

T8 + ME9.6 · 3 videos · 7 shim logs

🟡 Next

4 queued
A15 — staged GMLAN VIN proof ($241/$641 RDBI $90)
Immediate missing artifact · OBD ECM VIN already proven · do not lead with another $0541 replay
Next
Bojer's 2026-06-17 staged path still stands after today's map correction. Isolated T8 can help only for HS-CAN 500k proofs (OBD $7E0 VIN already done; next is ECM VIN over $241/$641 RDBI $90). Then CIM VIN with Tester Present, then I-bus, then seed/key. Do not soak a 3-box CIM/SCL/ISM harness as the first move, and do not treat a 1 MB T8 flash as a SAS oracle.

Evidence: reports/firmware-re/bojer_alignment_audit_20260617.md, reports/firmware-re/bojer_conversation_summary_20260617.md, reports/firmware-re/j2534_python_pro_bringup_20260616.md.
Fixture Tests (A14)
Lock behavior before field use — 2996 (11 tuples), 2017 (9 tuples), 1367 (9 tuples)
Next
Verified fixtures:
• 2996: VIN YS3FD79Y276102996, code O6IFISUM, 11 SKA tuples
• 2017: VIN YS3FD49YX41012017, code XJOUQVL7, 9 SKA tuples
• 1367: VIN YS3FH46U681101367, code NHOHNJPL, 9 SKA tuples

API endpoint: https://relevant-diann-djfremen2-c013cdc3.koyeb.app/api/ssa
All keys verified against Bojer oracle via live API — exact match on SKA blocks and ASCII codes.
Garage Schema Expansion (A09)
Room version bump — paint, drivetrain, HWKID, provenance
Next
New fields: paint name/code/hex, body_display, engine, gearbox, drivetrain, factory option count, HWKID, validation provenance, last SSA status, offline-ready timestamp. Remove raw seccode fields from production.
Protected API Lockdown
Tighten response surface — only safe metadata + 8-byte code
Next

📦 Projects

8 active · 3 parked
Tech2Win-Parrot — MVCI driver that shows up as its own adapter NEW 2026-05-26 PM · forked from dpdu-passthru · blocked on vcilib.c 1124 1 · parrot backend ready (FremSoft engine in attic)
Success metric: Tech2Win's "Select Vehicle Communication Interface" dialog lists Tech2Win-Parrot as its own selectable adapter (independent of any hardware). User picks it → Tech2Win drives ECU info panel render from recording data with no car connected. Binary win: does Tech2Win-Parrot show in the adapter list AND drive an ECU info panel.

v0.1 scope (ECU information slice): 1A xx data identifiers only — stateless lookups. All target bytes already exist in the 27 non-low-value Tech2Win sessions decoded 2026-05-26 morning. No seed/key, no flow control state machine.

Architecture — MVCI driver (registers itself in Tech2Win's adapter list):
Tech2Win.exe
   │  MVCI driver lookup
   │  (HKLM\SOFTWARE\D-PDU API\Modules\Tech2Win-Parrot)
   ▼
┌──────────────────────────────────────┐
│  Tech2Win-Parrot driver.dll          │
│  D-PDU API surface (29+ exports)     │
│   ↓                                  │
│  ComLogicalLink + ComPrimitive       │
│   ↓                                  │
│  ISO15765ComPrimitive::SendRecv()    │
│   ↓                                  │
│  Recording lookup ───►  recordings/*.json
│   ↓                                  │
│  PDU_IT_RESULT event back to Tech2Win│
└──────────────────────────────────────┘
            
Forked from dpdu-passthru iso15765. We already know Tech2Win lists this driver in its adapter selector — it did during today's 8 bench iterations. The wall is vcilib.c 1124 1, AFTER selection, before UDS flows.
Today's three-architecture detour — honest log:
  1. 5:13 PM: MVCI driver scaffolded from dpdu-passthru. Right architecture, blocked on vcilib.
  2. 5:41 PM: Pivoted to DLL-shim (FremSoft approach) on grounds that "vcilib feels too hard; shim is proven via Collector." 🚫 Wrong call — the DLL-shim doesn't appear as a selectable adapter, which is what the user actually needs.
  3. 6:13 PM: Cross-compiled FremSoft engine + 29-export shim on Mac (135KB DLL, MinGW-w64 i686). Deployed to .59. Tech2Win loaded our shim clean — but Vehicle Communication Interface dialog was blank (no adapter shown). DLL-shim architecture confirmed unfit for purpose.
  4. 6:31 PM: Reverted .59 to OEM. Rolled Parrot back to MVCI architecture. FremSoft engine code (recording.c, scheduler.c, fremsoft.c, unknown_log.c — proven to compile) moved to _attic/dll-shim-experiment-2026-05-26/ for reuse as the parrot backend on the MVCI side.
Net result: back at the right architecture with the parrot-backend code already built and compile-tested. The path forward is unambiguous.
Status: 🔴 MVCI driver fork in place; blocked on vcilib.c 1124 1 (same wall as dpdu-passthru).

Next session execution path:
  1. Run the IDA Pro breakpoint workflow at docs/ida_bp_vcilib_1124_runbook.md on emulator.exe to identify the failed precondition
  2. Fix in driver code (likely PDUSetUniqueRespIdTable STUB or PDU_EVENT_ITEM struct shape — see suspect ranking in runbook)
  3. Verify Tech2Win-Parrot moves past adapter selection
  4. Wire ISO15765ComPrimitive::SendRecv to the FremSoft engine from _attic/
  5. Install on .59 — see Tech2Win-Parrot in the adapter list, ECU info panel render
Engine code preserved in _attic/: 430 lines fremsoft.c + 250 lines recording.c + 186 lines scheduler.c + 85 lines unknown_log.c. Builds clean (MinGW-w64 i686 cross-compile from Mac, 135KB DLL output verified 2026-05-26 PM). The 29 PDU-export shim wrappers in _attic/shim/cstech2win/ are reference-only — not used on the MVCI side.

Repo home: local at /Users/admin/.openclaw/workspace/Tech2Win-Parrot/.
Tech2Win Installer — install · rename · verify · bundle NoMoreGlobalPy NEW 2026-05-26 · one-shot bench bootstrap · replaces the manual 4-stage dance
Why this exists: standing up a fresh bench (EliteBook, VM, anyone-else's machine) currently takes ~30 minutes of error-prone manual steps — install Chipsoft J2534 Pro Driver, install Tech2Win, rename CSTech2Win.dllCSTech2Win_real.dll, drop in the shim, install Python + NoMoreGlobalPy deps, hand-verify each layer. We've burned this loop on .59, .80, and during the dpdu-passthru iterations enough times to justify a single installer. Goal: bench-ready in one double-click.

What the installer does (in order):
  1. Install Tech2Win — runs the OEM Chipsoft J2534 Pro Driver setup + Tech2Win-2.336 setup silently (/S or InnoSetup /VERYSILENT). Drops CSTech2Win.dll + j2534_interface.dll at C:\Program Files (x86)\CHIPSOFT_J2534_Pro_Driver\.
  2. Rename OEM DLLs — moves CSTech2Win.dllCSTech2Win_real.dll and j2534_interface.dllj2534_interface_real.dll. Drops in the latest shim DLLs from Chipsoft_RE in their place. Same pattern OpenSAAB Collector uses, but standalone (no .NET service required).
  3. Verify — checks file SHA against expected manifest, confirms registry entries land, runs j2534_interface.dll patch-check (10-byte Boost.Log sink-ON), launches Tech2Win once to confirm the shim loads + writes its first log line. Bails loudly with the failed step if anything is off.
  4. Install NoMoreGlobalPy — bundles Python 3.12 embedded distro (no system Python required), drops NoMoreGlobalPy/ at C:\openclaw\NoMoreGlobalPy\, creates Start Menu shortcuts for vin.bat, csdirect.bat, pydpdu_seed.bat, seedkey.bat. app.py tkinter UI gets a desktop shortcut.
What's in scope:
  • Single tech2win-installer-vX.Y.Z.exe built via InnoSetup or NSIS
  • Embedded Tech2Win-2.336 + Chipsoft J2534 Pro Driver redistributables (license check: same redistribution policy as Collector uses)
  • Bundled shim DLLs pinned to a specific Chipsoft_RE commit SHA (no surprise drift)
  • Bundled Python embedded + NoMoreGlobalPy/ snapshot pinned to a workspace SHA
  • Verify step is its own subcommand — tech2win-installer.exe /verify re-runs the SHA + registry + shim-load check anytime
  • Clean uninstall — restores all _real.dll files byte-for-byte, removes the NoMoreGlobalPy bundle, leaves OEM Tech2Win install alone
What's NOT in scope (yet):
  • OpenSAAB Collector — separate installer; this one is for users who want the bench tools without the upload-to-R2 service
  • dpdu-passthru custom driver — different target (custom MVCI driver, not OEM Chipsoft), keep separate
  • USBPcap — Collector already handles this; not duplicating
Open questions for next session:
  • Installer toolchain: InnoSetup (matches Collector) vs NSIS vs MSIX. Recommend InnoSetup — already proven in Collector, same patterns reusable.
  • Tech2Win + Chipsoft redistribution: legal/license check before bundling vs prompting the user to drop installers in a known folder.
  • Repo home: new djfremen/Tech2Win-Installer repo, or live inside Chipsoft_RE as a sibling to the shim source?
  • Version policy: bump on every shim SHA pin or every NoMoreGlobalPy SHA pin? Recommend semver where minor = shim/Python bump, patch = installer-script-only.
Status: 🔴 not started · scoped today 2026-05-26 PM. Pickup point next session: pick installer toolchain (InnoSetup vs NSIS) and decide repo home, then scaffold the .iss / .nsi.
Cloud-driven SAAB diagnostics — web portal + bench relay NEW 2026-05-23 · Tech2Win lives in cloud · bench is a thin USB→PDU relay · two phases (data collection now → full control later)
End-state UX: user visits a web page, clicks "Connect to Chipsoft" → session opens. Clicks "Pull VIN" → car panel renders (year, color, engine, model from /api/lookup/{vin}). Then buttons: "Pull DTC", "Add Key", "Get SAS seed". Each fires a cloud workflow that drives Tech2Win (in cloud) → relays PDU calls over network → bench Chipsoft adapter → car. Result streamed back to browser. Every interaction logged on the backend.

Architecture (cloud is the engine, bench is the relay):
USER UX                CLOUD                              BENCH (thin relay)            CAR
┌──────────┐    ┌────────────────────┐    ┌─────────────────────────────┐    ┌─────┐
│ Browser  │ →  │ Web portal +       │    │ shim_server.exe             │ →  │ CAN │
│ (buttons)│ ←  │ workflow runner +  │ ←→ │ - receives PDU calls        │ ←  │     │
│          │    │ Tech2Win/replay    │    │ - invokes CSTech2Win_real   │    │     │
└──────────┘    │ engine             │    │ - returns response          │    └─────┘
                │ saab_codes.db ✅   │    │ - streams shim log up       │
                │ /api/process ✅    │    └─────────────────────────────┘
                │ /api/lookup ✅     │
                └────────────────────┘
                       ▲
                       │ interaction log (every cmd + response)
                       ▼
                ┌─────────────┐
                │ R2 / PG     │
                └─────────────┘
            
Latency model: network relay is at the PDU primitive boundary, not USB packet level. One PDUStartComPrimitive = one network round-trip (~50ms WAN, ~5ms LAN). A full Tech2Win seed-pull session is ~150 PDU primitives → ~8s relay overhead over WAN, ~1s over Tailscale. ISO-TP framing happens inside CSTech2Win_real.dll on the bench — never crosses the network — so CAN timing is preserved.

Crash isolation:
  • Cloud Tech2Win/runner crash → web session timeout, cloud restarts process, user re-clicks. Bench shim drops CAN session on idle timeout.
  • Bench relay crash → shim_server watchdog restarts; cloud sees PDU timeouts and retries.
  • Network hiccup → WebSocket auto-reconnect; resume if within 60s window.
  • No physical adapter to "crash" in cloud — cloud only manipulates PDU primitives; physical bus stays on the bench.

Phase A — Data collection (in flight; ~75% done)

Get every Tech2Win byte the community generates safely uploaded so we can build the workflow library + train the cloud engine. Most of this already ships.

  • OpenSAAB Collector — bench-side tray + service, monitors %TEMP%\cstech2win_shim_*.log, uploads to R2 with consent. v0.4.1 live on .59 and .80; repo.
  • cstech2win shim deployed at C:\Program Files (x86)\CHIPSOFT_J2534_Pro_Driver\; bootstrap auto-arms options.json; DETACH breadcrumb names the produced CS driver log.
  • j2534_interface.dll byte patch (10 bytes) forces Boost.Log sink ON for J2534-side tools regardless of options.json drift.
  • R2 upload pipe (Cloudflare) — Collector → presigned-URL upload → R2 bucket opensaab-capture.
  • Bench redundancy — two mirrored EliteBooks (.59, .80) on Tailscale; either can serve as the bench relay.
  • 🟡 Run archiver in NoMoreGlobalPy — every csdirect.bat / pydpdu_seed.bat writes runs\<tool>_<ts>.txt for diff analysis (2026-05-23).
  • 🔴 Upload-side enrichment — currently raw shim log only; want VIN, session-id, workflow-tag stamped per upload for searchability.
  • 🔴 Cloud-side analyzer — DLQ for empty/low-value captures (heuristic already exists in Collector); parser that extracts UDS sequences per workflow.

Phase B — Long-term: cloud-driven control (sketched)

The full vision Chris described: Tech2Win runs in cloud, web portal controls it, bench is a passive PDU relay.

What's net-new (in dependency order):
  1. Network-relay shim. Split the existing CSTech2Win.dll shim across the network: client-side serializes every PDU API call to protobuf/JSON over WebSocket; server-side (bench) deserializes and invokes the real DLL. ~500 lines C + small Python bench server. All 29 exports already trampolined; 6 already instrumented — extend instrumentation to ALL exports as serialize/deserialize endpoints.
  2. Cloud Windows host. Koyeb is Linux-only; cloud-side Tech2Win or the workflow runner needs Windows. Candidates: Azure Windows VM, AWS EC2 t3.medium Windows, Hetzner cloud Windows, or a self-hosted Windows host on Tailscale.
  3. Workflow runner in cloud (replaces Tech2Win.exe). Python service that emits PDU primitive sequences captured from real Tech2Win runs. One Python function per workflow: workflow_pull_vin(), workflow_pull_dtc(), workflow_get_sas_seed(), workflow_add_key(). Each is a literal replay of the PDU primitive sequence we captured in the shim logs. This is the "Tech2Win as code, not as exe" path — more controllable, crash-resilient, observable than driving Tech2Win.exe via UI automation.
  4. Session/command HTTP+WS endpoints on saab-security-api:
    • POST /api/sessions → assigns an available bench agent, returns session_id
    • WS /api/sessions/{id}/stream → bidirectional event stream (progress, results, errors)
    • POST /api/sessions/{id}/cmd/{kind} → invokes a workflow
    • GET /api/sessions/{id}/log → full audit trail of interactions
  5. Interaction logger. Postgres or JSON-append-to-R2: (session_id, agent_id, ts, cmd_kind, request, response, latency_ms, status). Drives the audit-log viewer and feeds back into Phase-A data collection.
  6. Web client. Lightweight HTML/JS at sas.mysaab.info/console (or new dashboard tab): Connect → VIN → car panel → action buttons. Streams progress events via WS. ~500 lines.
MVP slice to validate the architecture (~1 week of focused work):
  1. Network shim handling one primitive (PDUConstruct) end-to-end: cloud calls "construct" → bench shim invokes real DLL → returns. Proves the wire format + latency.
  2. Add PDUGetVersion as second primitive. Two-end-to-end proves serialization scales.
  3. Wrap the full Tech2Win "Pull VIN" sequence (~30 primitives) as the first complete workflow. End-to-end test from a curl against cloud → bench → car → VIN string back.
  4. Stand up the minimal web page (2 buttons: Connect, Pull VIN).
Once that slice works, every subsequent workflow is "capture the Tech2Win shim log → translate to a Python sequence in the workflow runner → expose as a new endpoint." Each new workflow is ~1 day, not ~1 week.
Open questions:
  • Cloud Windows host: which provider? Latency from Koyeb-WAS region to a Windows VM matters for the orchestrator's cross-cloud calls. Decide before shim work starts.
  • Workflow definition format: do we keep the existing Chipsoft_RE/workflows/<name>/definition.json schema (already 5 workflows defined) or rebuild for the cloud runner? Vote: keep + extend.
  • Authentication: per-user session tokens? Per-bench device tokens? Both? Affects audit log schema.
  • Multi-user concurrency: only one user at a time per bench (CAN bus is serial). Need queue or "in use" lock per bench. Affects UX.
Multi-adapter coverage — MDI + VX Nano NEW 2026-05-15 · J2534 + D-PDU shims · adapters in transit
Why this matters: the byte-equivalence proof from today's pcap↔shim alignment (`OpenSAAB/docs/2026-05-15_pcap_shim_alignment.md`) means we can drive the Chipsoft directly without Tech2Win. Generalizing the Collector to capture from GM MDI and VX Nano (ordered, in transit) lets us cross-validate the protocol catalogue across three independent hardware paths — anything identical at the J2534 / D-PDU API layer is standard SAE/ISO; anything that differs is adapter-specific quirk to document.

Strategic insight: J2534 (SAE) and D-PDU (ISO 22900-2) are standardized APIs. One shim per *API* covers every adapter that implements it — not one shim per physical adapter. So with the existing j2534_interface.dll shim plus a new generic dpdu shim we cover Chipsoft + MDI + VX Nano + every other compliant adapter going forward.

Already shipped today:
  • OpenSAAB/docs/adapter_coverage_matrix.md — per-adapter DLL ↔ shim ↔ standard-API map; shim build process template; per-adapter open work checklists
  • Chipsoft_RE/shim/dpdu/ — scaffolded generic D-PDU shim: README, parameterised Makefile (make TARGET=mdi_api), src/dllmain.c, src/log.c, src/shim.h, scripts/gen_shim.py. Awaiting first real DLL to instrument.
Phase plan when adapters land:
  1. Day 1 (~2 hr each): plug each adapter in, point a J2534 client (TrionicCANFlasher) at it, install the existing j2534 shim with the adapter's DLL name, run a probe. If the shim captures the expected API call sequence → that adapter is covered with zero new code.
  2. If GM tools use D-PDU not J2534: gen_shim.py against the real mdi_api.dll, fill in wrappers.c modeled on CSTech2Win, make TARGET=mdi_api. ~1-2 days.
  3. Cross-reference the same Tech2-style action through all 3 adapters; diff shim logs to surface standard vs. quirk.
  4. Add to Collector installer — new [Files] + [Run] entries per shim. X-Capture-Source whitelist gets the new source name (one-line server change).
API capacity for the projected load (from the matrix doc): Koyeb free-tier instance handles 10×–100× current volume with two small code changes:
  • Move pcap summary to FastAPI BackgroundTasks so /ingest returns immediately (today's blocking parse is the only real bottleneck)
  • Stream-parse the pcap instead of reading whole file into memory (free-tier 256 MB ceiling matters when 4 concurrent uploads collide)
R2 free tier (10 GB) covers ~10,000 captures @ 1 MB. Beyond that: $0.015/GB = pennies. Koyeb free egress (100 GB/mo) covers ~50,000 captures @ 2 MB. Triage: bump to nano instance ($5/mo, no cold-start) at 20+ active contributors.

What this unlocks: protocol catalogue cross-validated against 3 independent hardware sources — the strongest reverse-engineering claim we can make. Plus a clean migration path: as adapters age out (Chipsoft Pro is increasingly hard to source), our shim coverage moves with the user.
OpenSAAB Collector v0.2.8-alpha — CSTech2Win shim struct fix BUMPED 2026-05-18 · upgrade strongly recommended (v0.2.5 ships broken-struct shim)
Critical upgrade: all prior installer releases (v0.2.5-alpha and earlier) bundle a CSTech2Win.dll shim with a 4-field / 16-byte T_PDU_PARAM_MIN struct missing the leading ItemType field. Per cross-reference with tradingalerts607/pdu_mdi2 and AUTOSAR spec, the canonical layout is 5 fields / 20 bytes with ItemType at offset 0. Every PDUSetComParam log line in old releases is shifted one slot — the repeating ComParamId=0x00001200 we all saw was actually ItemType=PDU_IT_PARAM (a struct marker, not a parameter id). Real values were silently shifted into adjacent columns. Anyone running v0.2.5-alpha is uploading mis-aligned ComParam data to /ingest.

What v0.2.8-alpha changes: bundles the struct-fixed shim from Chipsoft_RE@dbd807d. Only the bundled DLL is different vs v0.2.7 — no .NET service/tray changes. After installing, the next Tech2Win capture will show PDUSetComParam with the proper 5-column layout: ItemType=0x1200 ComParamId=0x000D00xx DataType=0x00000105 Class=1/3/5 pData=.... The Chipsoft 0x000D00xx vendor namespace is now visible and cross-referenceable against DiagProf/ISO22900.II's ISO_15765_4.cs named ComParams.

Upgrade path: uninstall the previous Collector (Start Menu → "Uninstall OpenSAAB Collector" — restores CSTech2Win_real.dll byte-for-byte), then run the new opensaab-collector-setup-0.2.8.exe. Service + tray + new shim re-installed in one pass.

Includes v0.2.0–v0.2.7 in flight: USBPcap one-click capture in tray, hub-wide capture safe across USB rotation, SHA256 dedup + pre-upload pcap integrity, tray "Stop capture" opens Explorer to the .pcapng, USBPcapCMD leak fix across service restarts.

Release page: v0.2.8-alpha — CSTech2Win shim struct fix (critical)
Diff analysis: pdu_mdi2 ↔ CSTech2Win Ghidra constant diff
OpenSAAB DTC catalog v1 — 1,735 WIS-sourced entries NEW 2026-05-18 PM · full per-module catalog · authoritative
What landed: per-DTC YAMLs under OpenSAAB/dtcs/<module>/<CODE>-<FT>.yaml for every DTC in the SAAB WIS catalog. 1,735 codes across 36 modules (ACC, ACM, AHL, AHM-PHM, AMP1, AMP2, BCM, CDCF, CDCR, CDF, CIM, DDM, DSM, ECM/Trionic-T8/Motronic-E9/Simtec/EDC16, ESP, EHU, ICM, MIU, OnStar, PDM, PHM, PSG16 diesel-pump, REC, RLDM, RRDM, SDM, SLM, SPA, SRM, TCM, TMC, TPMS, UEC, UHP). Crawled directly from saabwisonline.com/9-3-9440/2003/dtcs/ — covers all M03+ MYs since SAAB consolidated the DTC dictionary in MY03.

Schema (per YAML): dtc (e.g. B0260-06), module, wis.description (verbatim WIS title), wis.fault_tracing (multi-paragraph WIS notes preserved as YAML literal block), wis.url, wis.fetched_at. iOS / Android clients can pull a single YAML per DTC for live tooltip / diagnostic context.

Three corrections to existing intel surfaced by the WIS crawl:
  • B0260-06 was annotated in commands/saab/check_codes_read_dtcs.yaml:122 as "possibly seat-related" (z90.pl had no entry). WIS authoritative: Vent Door Motor Left Circuit, Open/Short to Ground — it's an HVAC actuator, not seat. Fixed.
  • SAAB does use U0xxx codes after all — but only on PSG16 (Bosch diesel pump ECU on EDC15) and with SAAB-specific semantics that differ from SAE J2012 (e.g. U0002-00 WIS = "PSG16 was Missing on Bus", SAE = "High Speed CAN Bus Performance"). My earlier "SAAB never uses U0xxx" blanket statement was overstated.
  • U1500 failure-types in WIS are only 01/0F/38, not 01/02/03/0F/38 as my earlier note claimed.
Why this matters for iOS: the dashboard JSON shadow (/api/dashboard.json) doesn't yet wire DTC YAMLs in — but a future endpoint like /api/dtc/<code> can serve the WIS-sourced description + fault-trace per-call from the catalog. iOS users see "B0260-06 → Vent Door Motor Left Circuit" instead of "B0260-06" alone.

Sources: saabwisonline.com DTC catalog · WIS deep survey 2026-05-18 · DTC enrichment status report
OpenClaw Operator API + Browser Console NEW 2026-05-11 · Tech2Win-as-API · 8/8 tests pass
Architectural pivot landed: all chipsoft + UDS + workflow smarts now live server-side. Clients (Android, browser, future iOS) become thin USB byte relays over WebSocket. Same API, many client types, one source of workflow truth.

Day 1 shipped:
  • operator_api/ Python module — chipsoft envelope (pcap-validated), UDS framing, Bojer client, Transport interface, workflow JSON runner, FastAPI router
  • 5 SAAB workflow definitions in Chipsoft_RE/workflows/ (bundled into Docker as operator_api/workflows/): module_presence_scan, vin_read, seed_sweep_l01, engine_sas_unlock, engine_ssa_writeback
  • Browser operator console at /operator — vanilla HTML/CSS/JS, no build tools, live color-coded console log, Bojer roundtrip buttons, local-key derivation, WebUSB chipsoft picker stub, export.log button
  • 8/8 unit tests pass — chipsoft envelope byte-locked against the 2026-05-08 TrionicCANFlasher USBPcap
  • HTTP endpoints under /operator/v1/: /workflows, /bojer/process, /security/key, plus a WebSocket stub at /usb-relay
What works today (no chipsoft, no bench):
POST /operator/v1/security/key — local seed→key (33/33 validated)
POST /operator/v1/bojer/process — proxy 714B pre-auth to Bojer
GET /operator/v1/workflows — catalogue listing
• Browser console UI fully wired to above

Day 2 (next): wire the WebSocket USB byte-relay so the server can drive a workflow through the browser's WebUSB connection to a real chipsoft. operator_api/transport.py already defines the interface; MockTransport covers the offline test path; WebsocketTransport is the missing piece.

Bigger picture: this is the foundation for the Tech2Win-as-an-API direction. Workflow definitions are JSON, validator-backed, version-controlled. New cars = new vehicle/ directory. The synthetic-shim sketch at Chipsoft_RE/shim/cstech2win/SYNTHETIC_MODE_SKETCH.md is the planned source of new workflow captures without bench dependence.
SAAB Security Access Flagship · API live on Koyeb · 2.72M DB rows
11/11Batch pass
7/7Oracle exact
4VINs verified
2026-05-05 — Tech2 chunked-read byte-order bug ROOT-CAUSED + fixed in saab_simple, SAABSecurityAccess, NoMoreGlobal_dotnet8_v8 (latent for as long as the C# Injector existed; corrupted every read past offset 0x100). Live-read post-Chipsoft Tech2 → 0 byte differences vs Chipsoft's own dump → full Bojer round-trip succeeds (HWKID S000310723, IMMO XJOUQVL7).
Koyeb Docker rebuild & deploy
Bojer mutation contract reverse-engineered
eSaab/GM factory enrichment (paint, body, engine, gearbox)
Inclusive terminology (Sedan/Saloon, Convertible/Cabriolet)
API response lockdown (safe metadata only)
Shared protocol spec Android/C#
Android Tech2 Client 42 Kotlin files · FTDI/FT232 field-proven · Pixel 7 tested
A01: Repoint API to live Koyeb
A02: Remove SeccodeDecryptor from production
A03: Fix 8-byte injection at 0x26..0x2D
A04/A05: SKA tuple parsing — fixtures replaced, offset 0x130 confirmed
A06: NativeVM opcode correction — verified against Table 1
Read chunk size 0x28 → 0x40 — Android now reads what C# reads
2026-05-05 — `buildChunkReadCommand` byte-order fix (BE 24-bit address); offset ≥ 0x100 was returning shifted page-0 garbage. Bench-confirmed against Chipsoft dump.
Auto-verify after write — re-reads device, updates UI without user tap
Bojer-first end-to-end field test on chassis 1012017 (XJOU/QVL7)
A07-A08: HWKID + Mutation Processor
A09: Garage schema expansion
A10: Recalls display
A14: Fixture tests — 3/3 verified via API
2026-05-05 — CH340/CH341 USB-serial cables now auto-trigger app via device_filter.xml (driver support already present via usb-serial-for-android 3.5.1 default prober — no new driver code needed)
FR-1 (filed 2026-05-05): Foreground service for active USB sessions — head-unit task killers + standby kill long-running USB workflows mid-operation. Wrap `SerialWorkflowService` with `startForeground()` + notification channel. Deferred until first-pass head-unit testing reveals exactly which scenarios bite.

Stack: Kotlin 2.0.21, AGP 8.13.0, min/target SDK 24/36, Retrofit 2.9.0, Room 2.6.1, Compose BOM 2024.09.00

C# Tech2Win + 3D Twin WinForms · GLB assets · ~30% match current API
NoMoreGlobal_dotnet8_v8 in workspace
2026-05-05 — `BuildChunkReadCommand` byte-order fix (BE 24-bit address); same latent bug as Android; patched in `SerialDeviceInteractor.Transport.cs`
Import/reconstruct real .sln/.csproj
Align to server mutation model
Shared GLB asset pack
Dynamic paint from eSaab/RPO
CIM Bench — Key Add/Modify (use roffe/eep) 🅿️ Parked — Arduino + SOP8 clamp + 93C66 · roffe/eep is the tool · started 2026-05-03

The work is done, open source. roffe/eep ("Saab CIM Tool") — Go GUI + Arduino UNO firmware + SOP8 clamp on the 93C66. Add Key dialog takes IDE (4-byte hex) + Sync (4-byte hex) and writes the cell. View / Edit / Hew also implemented. No CIM unlock, no UDS, no CAN — talks straight to the EEPROM chip in-circuit.

Project is now "build the fixture and use it," not RE the protocol. Earlier plan to reverse-engineer the CIM diagnostic write surface is obsolete — chip-level access bypasses the unlock entirely.

Build the Arduino UNO + SOP8 clamp + breadboard fixture per roffe/eep README
Flash AVR firmware (firmware/ or firmware_v2/)
Document where the 93C66 sits on each CIM PCB revision (photos)
First Add Key dry run on bench CIM, snapshot before/after
(Stretch) Integrate with Garage workflow — export key list per VIN
(Stretch) Coordinate with Bojer's PCF-write tool for transponder-side programming

Key format already known: 8 bytes per key = 4-byte IDE + 4-byte Sync.
Full notes: wiki/projects/cim-bench-key-tool.md

Chipsoft J2534 — Android-Direct USB-CDC Live ✅ 2026-05-07 — Pixel 7 talks Chipsoft Pro directly; SET_CONFIG/GET_CONFIG/Open empirically confirmed; STARTCOMM still unmapped

Goal: understand the Chipsoft J2534 Pro adapter at the wire level so the Android client can talk to it directly over USB-CDC, no Windows driver required. This is the path-of-least-friction transport for an Android-direct SAAB unlock against a SAAB on a real OBD port.

Static RE — DONE. Five rounds of Ghidra analysis on j2534_interface.dll. Every layer mapped: USB-CDC virtual COM transport, 8-byte wire envelope (cmd, len, reserved, sum-mod-0xFFFF checksum, payload), IOCP queue / drain thread, and the FUN_1001d270 send-and-wait dispatcher with all 24 opcodes catalogued and traced back to their PassThru* exports.

Algorithm correctness — DONE. Trionic 8 + ME96 seed→key validated against 45 captured (seed, key) pairs / 3+ years of TrionicCANFlasher logs. 100% match. Kotlin port is unblocked.

options.json fully decoded — DONE. LogLevel scale: 0..4 enables Boost.Log sink (0=trace, max verbosity), ≥5 disables. Default log path: %ALLUSERSPROFILE%\CHIPSOFT_J2534\logs\<YYYYMMDD>_<HHMMSS>.log. Tier sub-objects: Lite / Mid / Pro, each with OpenPort2Mode / RemapAUXToPIN / SplitReadTimeout.

Firmware bins (canhacker_pro.bin / j2534_pro.bin / kline_pro.bin) — BLOCKED, not on critical path. Format is [4-byte LE length][encrypted payload]. Payload entropy 7.99/8 bits/byte; single-byte XOR and repeating-key XOR ruled out (IoC = uniform random across all periods 1..2048). binupdate.exe contains zero crypto constants — it's a pure passthrough; decryption happens in the on-device STM32 bootloader. Recovering would need SWD/JTAG against a physical device with RDP not engaged. Unnecessary for the Android-client objective.

DLL shim project — REVIVED. Tech2Win uses CSTech2Win.dll (D-PDU/ISO 22900-2), not j2534_interface.dll, so the j2534-shim plan was the wrong target. Built and shipped a CSTech2Win shim instead — captures every D-PDU API call and the underlying UDS PDUs. 30+ shim runs landed; Tech2Win's per-channel init pattern recovered: PDURegisterEventCallback → PDUIoCtl(0x000C0001) → PDUIoCtl(0x000C0000) → PDUStartComPrimitive(0x8001 STARTCOMM) → PDUStartComPrimitive(0x8004 SEND/RECV).

Android-direct USB-CDC — LIVE 2026-05-07. Pixel 7 + powered USB-C hub talks the Chipsoft Pro J2534 adapter directly. Firmware string returned via opcode 0x01: CHIPSOFT J2534 Pro v. 1.5.2. Empirically confirmed wire shapes: 0x0B = PassThruIoctl SET ([ChannelID 4B LE][Parameter 4B LE][Value 4B LE], empty-payload ack); 0x0C = PassThruIoctl GET (same shape, returns current value as 4B LE payload). Channel 1 round-trips DATA_RATE=33 333 + LOOP_BACK=0 + J1962_PINS=0x0101; GET reads back what SET wrote. Full session lifecycle (0x20 → 0x01 → 0x08 Open, 0x20 Close) round-trips cleanly.

Standalone bench app — github.com/djfremen/Chipsoft-Android. Single Compose screen, three sections of probe buttons (session lifecycle / SAAB SWCAN init / SecurityAccess wire). Per-session capture file at <external>/captures/chipsoft_<ts>.log for adb pull. Repo also carries a curated reference/shim-captures/ with 15 named shim logs (YYYY-MM-DD_HHMMSS_<topic>.log convention: seed-exchange-c4dc, ecu-info, vehicle-voltage, seed-nack-only, etc.) so wire ground truth lives next to the app code.

Open: STARTCOMM-equivalent J2534 wire opcode. Tech2's D-PDU layer uses PDUStartComPrimitive(0x8001) to bring a channel online before any Filter / WriteMsgs. Empirically probed every un-mapped J2534 ioctl from Android (0x07/0x11/0x12/0x18/0x1C/0x23) — only 0x1C returned a clean SET-ack, but even after firing it the bus stayed silent on subsequent Read polls (40+ polls, all BD 00 00 00). Without STARTCOMM the channel installs filters and accepts WriteMsgs but nothing actually transmits. Cleanest unblock: USBPcap of TrionicCANFlasher session on Win10 (TrionicCANFlasher uses j2534_interface.dll directly = same wire envelope Android-direct mimics).

Round 1-2: PE summary, exports, USB-CDC transport identified
Round 3: framers + IOCP drain + transport mapped
Round 4: wire envelope (8-byte header) + sum-mod-0xFFFF checksum
Round 5: full 24-opcode catalog via DumpOpcodes.java
Round 6: options.json reverse-engineered
Firmware encryption analysis — blocked, documented
CSTech2Win shim built; 30+ raw captures; Tech2 init pattern recovered
Bench session: SecurityAccess $27 0B on engine ECM $0241 wire-confirmed; seed 0xC4DC deterministic across runs
Android-direct USB-CDC handshake live; 0x01/0x03/0x08/0x0B/0x0C/0x0D/0x20/0x21 empirically confirmed
SET_CONFIG / GET_CONFIG wire shape confirmed (channel-1 SWCAN @ 33 333 / pin 1 / loopback off)
Standalone chipsoft-android repo with curated shim-capture reference
USBPcap of TrionicCANFlasher → recover STARTCOMM-equivalent wire opcode
Live $27 0B seed exchange + $27 0C 4E ED unlock from Android-direct
Read full 714-byte SSA blob from ECM directly (via $23 ReadMemoryByAddress or replay of Tech2's read sequence) — byte-identical to /tmp/bench_pre_auth.bin

Repos: djfremen/Chipsoft_RE (RE notes + shim source-of-truth) · djfremen/Chipsoft-Android (bench app + curated wire reference)
Latest notes: Chipsoft_RE/notes/2026-05-07-{handoff-ssa-direct-read,bench-runbook-seed-capture-and-app-validation,sas-monitor-feature-request}.md
Day's full bench journal: memory/2026-05-07.md in workspace

FremSoft — Tech2Win plays a ghost car 🌵 [MERGED INTO TECH2WIN-PARROT 2026-05-26 PM] Renamed + finished as Tech2Win-Parrot on 2026-05-26 PM · same architecture, clearer name · FremSoft repo retires once Parrot builds clean on Windows

What it is: a behavior mode added to the existing CSTech2Win.dll shim (and parallel J2534 shim). When the registry flag is set to playback, the shim stops forwarding to the real Chipsoft DLL and serves UDS responses from a pre-recorded JSON session. Tech2Win cannot tell the difference — that's the point. Name = Fremen + soft, with a pun on Chipsoft.

Three layers of value. (1) Replay — demo Tech2Win against a ghost car, no ECM connected; useful for dogfooding, training, repair walk-throughs. (2) Probe capture — unrecorded requests get answered 7F 11 ServiceNotSupported and logged to %TEMP%\fremsoft_unknown_*.log; Tech2Win itself walks every menu, the log becomes a self-discovered protocol map. (3) Module emulation — once every request is known, build per-module responders (fake CIM/BCM/ECM) producing internally consistent state. Tech2Win drives a complete software-only SAAB.

Registry mode switch. HKLM\SOFTWARE\OpenSAAB\Collector\Mode ∈ {passthrough, record, playback, standalone}. PlaybackRecording points at the JSON file. Helper .cmds already ship in the Collector installer at installer/fremsoft/enable-record.cmd + enable-playback.cmd.

Recording format. Indexed JSON, lookup key (can_id, uds_bytes). Converter tools/build_recording.py (Python, Mac side) ingests a CSTech2Win shim log and emits the JSON. First recording landed: recordings/2026-05-13-check-codes.json from yesterday's bench Check-Codes capture.

Live activity log → decoder pipe. Playback emits %TEMP%\fremsoft_<wall_ms>.log in the same line format as the existing CSTech2Win shim. The Collector's bundled fremsoft-decoder.exe (PyInstaller scapy dissector) tail-pipes it with no special-casing; the tray already recognises fremsoft_* as a third log family alongside cstech2win_shim_* and j2534_shim_*.

Day-1 design doc (docs/design.md)
Per-export categorization + C source skeleton (headers)
29 D-PDU exports mapped for CSTech2Win standalone mode
14 J2534 PassThru* exports mapped for parallel shim
L1 implementation: recording loader + scheduler + delivery thread + unknown-request logger
jsmn vendored as header-only JSON parser
MSVC build auto-runs fix_msvc.py before cl; path portable
Mac-side recording converter; first JSON from 2026-05-13 Check-Codes capture
fremsoft_*.log spec wired so the bundled scapy decoder ingests with no special-casing
Build playback shim on EliteBook over SSH (next bench session)
Install Collector with Mode=playback; point at 2026-05-13-check-codes.json
Launch Tech2Win + open Check Codes — expect same DTCs (incl. B3832-45 on doors) with no ECM
Inspect %TEMP%\fremsoft_unknown_*.log for any out-of-recording requests = protocol-map data points
L2: tray-UI mode switch, multi-recording library, OpenSAAB-catalog synthesis fallback

Repo: github.com/djfremen/FremSoft — v0.1.0 tagged 2026-05-14
Split history: lived under Chipsoft_RE/fremsoft/ 2026-05-13; bundled into Collector v0.1.3-alpha; bundling reverted and FremSoft moved to its own repo 2026-05-14 to keep Collector (community data collection) and FremSoft (replay/ghost-car) goals cleanly separated
Docs: docs/design.md, docs/standalone-export-map.md (29 D-PDU), docs/standalone-export-map-j2534.md (14 J2534)
Wiki: wiki/projects/fremsoft.md

ECM Freshness Investigation 🅿️ Parked — bench rig + OBDLink SX · UDS 0x27 staleness probe · started 2026-05-03

Objective: empirically pin down what makes the SAAB ECM treat written SSA bytes as fresh vs stale, so the Android detector can reliably distinguish "previously enrolled" from "session-authorized." Feeds the on-device 3-table runtime decision.

Working hypothesis: staleness is coverage-based, not time-based. ECM picks a seed from an internal pool at access; if our pre-written SKA (seed, key) tuples don't cover the asked seed, ECM rejects with 0x35 invalid key. Time delay (0x37) is a secondary lockout after failures.

Two bench rigs: ECM tabletop (CAN @ 500k via OBDLink SX) probes UDS 0x27 → SKA tuples; ISM tabletop (K-line via VAG KKL) probes IMMO codes. Different modules behind the same SSA byte layout — separates "ISM accepts vehicle-bound IMMO" from "ECM accepts session-bound SKA."

Confirm bench ECM platform (T8 / T7 / ME96 / CIM)
Write tools/bench_probe.py — pyserial + ELM327 AT, scripts UDS 0x27 handshake
Run experiment matrix: cold-start, post-auth, power-cycle, wrong key 3×, lockout, idle timer
Confirm whether flash-written SSA bytes are consulted during live CAN handshake
Refine 3-state detector in Android (PRE / ENROLLED / POST_AUTH-fresh)
Capture results in reports/ecm-freshness/

Resources: saab_security_project/external/Trionic/ (cloned, gitignored — full UDS 0x27 reference + SeedToKey + ELM327 driver + error code table); table_1_v13.bin + execute_step VM (77/77 captures verified); tools/ssa_inspect.py.
Full notes: wiki/projects/ecm-freshness-investigation.md

🔧 Ops

Sentinel
Sentinel Health Checks
4x daily at 6/12/18/0 HST · Moonshot model · Telegram delivery
Active
Fixed: Model switched from Ollama to Moonshot (auth issue)
Schedule: 2x → 4x daily
Pending: Expand beyond gateway health checks, fix audit timeout, Telegram grammy warning

🔗 Quick Access