# MAG160C Reverse Engineering Progress ## 2026-07-12 - Started task. - Read relevant process skills: using-superpowers, brainstorming, planning-with-files, test-driven-development, systematic-debugging, verification-before-completion. - Confirmed current directory is not a git repository. - Ran initial file inventory with rg --files; found Windows C# examples, native SDK headers/DLLs, Android demo/AARs, native libraries, and libuvc/libusb source. - Created persistent planning files. - Extracted SDK PDF text with Python/pdfplumber. - Read Windows headers and C# wrappers. - Read Android demo source and AAR/class signatures using javap. - Extracted thermallib-release.aar to .reverse/thermallib_aar for analysis. - Confirmed IR camera VID/PID 0x833C/0x0001 from Hardware Guide. - Confirmed Android native libcoresdk.so exports MAG_* APIs and uses libusb. - Attempted to install capstone/pyelftools into .tools/python-revlibs; failed because network/proxy is unavailable. No new reverse-engineering software installed. - Tried PowerShell Get-PnpDevice for attached hardware; access denied in current environment. - Tried MinGW objdump on ARM64 libcoresdk.so; unsupported architecture. - Resumed from planning files and continued Android bytecode inspection with javap. - Found detailed FTDI/TCM command framing in FTDICommand and confirmed Android TCM bulk communication auto-detects an interface with bulk IN + bulk OUT endpoints. - Expanded TCM command table for motor, proximity, light, power, audio, schedule task, and firmware update commands. - Extracted MagDevice JNI method surface and core native data structures from coresdk.jar. - Inspected Windows ThermalSDK.dll, CameraSDK.dll, and CoreSDKLib.dll exports/imports/strings. - Confirmed ThermalSDK.dll wraps CoreSDKLib.dll MAG_* APIs, CoreSDKLib.dll imports libusb0 bulk read/write APIs, and Windows TCM logs match the Android TCM command frame model. - Marked reverse-engineering evidence phases 3 and 4 complete; moved into design approval phase. - User selected Linux deliverables: C/C++ library plus CLI sample, and Python-callable tooling/bindings. - User approved implementation approach 1: C++ core library + stable C ABI + CLI + Python ctypes/cffi-style wrapper. - Wrote design spec at docs/superpowers/specs/2026-07-12-mag160c-linux-sdk-design.md and ran a placeholder scan/self-review. - Did not commit the design spec because the workspace is not a git repository; progress remains tracked in markdown files. - User approved the design/spec and requested the implementation plan. - Wrote implementation plan at docs/superpowers/plans/2026-07-12-mag160c-linux-sdk.md. - Ran implementation-plan self-review/placeholder scan; no red-flag placeholders were found. - Implemented Task 1 scaffold: CMake, public C ABI lifecycle, error names, and lifecycle tests. - Task 1 verification note: cmake is unavailable in PATH, so substituted direct MinGW g++ compile/run verification with MAG160C_STATIC and MAG160C_HAS_LIBUSB=0; test executable passed with exit code 0. - Implemented Task 2 TCM frame codec with byte-exact encode/decode tests and C ABI utility functions. - Task 2 verification note: cmake remains unavailable in PATH, so substituted direct MinGW g++ compile/run verification for test_c_api and test_tcm_frame; both passed with exit code 0. - Implemented Task 3 TCM command builder, dry-run C ABI helpers, and command payload tests. - Task 3 verification note: cmake remains unavailable in PATH, so substituted direct MinGW g++ compile/run verification for test_c_api, test_tcm_frame, and test_tcm_device; all passed with exit code 0. - Implemented Task 4 USB device model, endpoint-pair selection, and C ABI device listing/freeing. - Task 4 verification note: cmake remains unavailable in PATH, so substituted direct MinGW g++ compile/run verification for test_c_api and test_device_model; both passed with exit code 0. ## 2026-07-14 Handoff - User asked to stop and then requested a compact context handoff for a new conversation. - Updated `task_plan.md`, `findings.md`, and `progress.md` with a short resume-ready summary. - Current implementation state to remember: - design and plan are already approved - Tasks 1-4 are implemented and verified in the no-libusb fallback path - remaining work is the live Linux runtime path, CLI sample, and Python wrapper integration - Important stop condition: - do not continue execution unless the user explicitly asks to resume work ## 2026-07-14 Resume - User explicitly resumed the task and added a Linux SDK under `IR_Camera_SDK-1.0.1/linux`. - Restored context from `task_plan.md`, `findings.md`, and `progress.md`. - Attempted planning-with-files session catchup via `.claude` skill path; it failed because `C:\Users\ZXC\.claude\skills\planning-with-files\scripts\session-catchup.py` does not exist in this Codex install. - Verified the workspace is still not a git repository, so implementation continues in-place with markdown progress tracking. - Began Linux SDK inventory. Found versioned Linux packages for 1.0.0, 1.0.1, 1.2.1, and 1.2.2; 1.2.2 is extracted with Qt demo sources, headers, Linux shared libraries, and firmware. - Read Linux 1.2.2 `CHANGELOG`, `Coding Tips`, `ThermalSDK.h`, `CameraSDK.h`, `Def.h`, and Qt project file. - Inspected Linux `libthermalSDK.so` and `libmagcore.so.2.1.1` ELF symbols/strings. Confirmed `libmagcore` exports `MAG_*` plus bundled `libusb_*`; confirmed Linux TCM implementation uses `SerialPort`/`TCMDevice::open(std::string)` classes around the same FTDI command concept. - Extracted text from Linux `SDK Guide.pdf` and `Quick Start.pdf` after two PowerShell quoting failures; root cause was Bash-style heredoc/Python newline quoting in PowerShell, not PDF tooling. - Read Linux Qt demo `basewidget.cpp`, `testwidget.cpp`, `expertwidget.cpp`, and `smartwidget.cpp`; recorded startup/callback/calibration usage in findings. - Disassembled small Linux x86-64 slices of `MAG_LinkCamera`, `MAG_LinkCameraEx`, `MAG_StartProcessImage`, `MAG_TriggerFFC`, `MAG_EnumCameras`, `MAG_GetCamInfo`, `MAG_SetFFCMode`, `EloMotorDevice::rotate`, and `TCMVirtualDevice::sendData` to extract interoperability facts only. - New low-level Linux evidence recorded: IR endpoints `0x03` OUT, `0x81/0x82/0x84` IN; raw stream markers `0x1bb1b11b/0x1bb1b11c`; `MAG_LinkCamera` default VID `0x833c`; Linux rotate clamp `[-250,250]` while Android evidence had `[-128,128]`. - Implemented Task 5 IR API skeleton with public `mag160c_ir_info_t`, `mag160c_ir_open_first`, `mag160c_ir_close`, `mag160c_ir_get_info`, `mag160c_ir_trigger_ffc`, and `mag160c_ir_read_raw_once`. - Task 5 RED verification: direct MinGW compile of `tests/cpp/test_ir_skeleton.cpp` failed as expected because `mag160c_ir_*` and `mag160c_ir_info_t` were not declared. - Task 5 GREEN verification: direct MinGW compile and run of `build_task5_ir_skeleton_test.exe` passed with exit code 0. - CMake is still unavailable in PATH (`cmake --version` failed), so full Task 5 verification used direct MinGW no-libusb builds. - Full no-libusb direct verification after Task 5 passed: `test_c_api`, `test_tcm_frame`, `test_tcm_device`, `test_device_model`, and `test_ir_skeleton` all compiled and ran; output ended with `ALL_DIRECT_TESTS_PASSED`. - Implemented Task 6 `mag160c-cli` with `probe`, `tcm-rotate --dry-run`, `tcm-light --dry-run`, and `ir-info`. - Task 6 RED verification: direct MinGW compile of `tools/mag160c_cli.cpp` failed as expected because the CLI source file did not exist. - Task 6 CLI verification: `build_task6_mag160c_cli.exe` no-args printed usage and exited 1; `tcm-rotate --dry-run 5` printed `7e 00 07 85 02 77 00 01 00 05 7f`; `tcm-light --dry-run green blink` printed `7e 00 09 87 02 32 00 01 01 00 ff 00 35`; `ir-info` printed MAG160C skeleton info and known Linux endpoint evidence. - Full no-libusb direct verification after Task 6 passed again for `test_c_api`, `test_tcm_frame`, `test_tcm_device`, `test_device_model`, and `test_ir_skeleton`; output ended with `ALL_DIRECT_TESTS_PASSED`. ## 2026-08-10 Deep Disassembly: Full Protocol Recovery - Network became available; pip-installed capstone 5.0.7 + pyelftools 0.33 into `.tools/python-revlibs` (verified: r2pipe/angr/keystone/unicorn also installed). Built reusable `analysis/revtools.py` (ELF loader, ARM64 PLT/GOT resolver, lite C++ demangler, xref scan) and used MinGW objdump/readelf on the x86-64 Linux `libmagcore.so.2.1.1` and Windows `CoreSDKLib.dll`. - Recovered the complete vendor USB protocol from libmagcore (was the #1 open question): - Link: VID 0x833C, set_configuration(2), claim_interface(0), auto-detach on, optional pid/bus/addr filters. - Command channel: EP OUT 0x03 + EP IN 0x82 (0x1000, 2000ms); 8-byte packets `{u32 magic, u32 param}`; 0x3c-byte packets for DDT. - Command magic table: 0x6bb6b66b/66c prepare, 66d/66e DDT read/write (0x38-byte payload), 66f camera info, 670 version, 671 (Windows-only), 672 FFC, 673 start (after 50ms + thread setup), 674 stop, 676/677 set params (Linux-only). - Response magic table: 0x5bb5b55b/55c/55d/55f = 0x38-byte info blocks (55b: pid@0, width@0x10, height@0x14), 55e = 16-byte qword pair, 57b = saved-file header. - Frame stream EP 0x81: marker 0x1bb1b11b@0, frame counter@4, data length@8, type@0xc (0/1), shutter@0x10, pixel data@0x1c (uint16 LE), trailing 0x1bb1b11c@0x1c+len, total 0x38+len, width 160 or 384. Reader thread 0x170a4 + dispatcher 0x16fb5. - Windows CoreSDKLib.dll cross-validates: same magic codes (66b/66c/66f/670/671/672/673/674), same frame markers with 0x1c offset, same 50ms-then-start flow, same 0xe4ae error domain, same response dispatch shape (0x5bb5b55b..0x5bb5b560). - Recovered temperature conversion math from ARM64 libcoresdk.so exported CFunctions: - Calibration (0x6a41c): per-pixel PWL - diff=(raw-baseline)>>1, band search on per-pixel threshold table (stride bands-1), v=off[i][p]+(diff*coeff[i][p]>>12), clamp [0,0xffff]. - ConvertResponse2Temperature (0x73124): base=(50000-gain)>>shift + diff*coeff, auto-gain decrement when >0x101 pixels saturate. - CAccumulator::PushFrame (0x62548): baseline copy/avg accumulation with restart state machine. - GetTemperature (0x7252c): window average (radius 1..3) -> ReviseTemperature (0x6cdb8, T2E PWL 0x112 entries + emissivity float mixing + inverse-map binary search) -> CorrectTemperature (0x6d6b0, offset segmentation k=(v-15000)/100 clamped 0..6, float gains +0xc4/+0xcc/+0xd0). - T2E table symbol exported (`T2E` @ 0x402010), monotonic 1000..737212, dumped to analysis/t2e_table.json; float constants 1000.0/0.01/0.99; units consistent with millidegree-C scale. - DDT saved file: 0x5bb5b57b header + aligned payload + 0x5bb5b55b/55c/55d/55e/55f typed blocks. - Wrote complete spec to `analysis/protocol_spec.md`; updated `findings.md` with the 2026-08-10 session summary. - Implementation updates (all passing direct MinGW builds, same method as before): - include/mag160c/mag160c.h: frame header struct, all magic constants, temp-conversion constants. - src/core/ir_frame.{hpp,cpp}: recovered frame parser (parse_ir_frame) + Calibration piecewise-linear converter (calibrate_frame). - tests/cpp/test_ir_frame.cpp: 7 tests (frame parse valid/bad marker/bad type/bad trailer/truncated, calibration band select/interpolation+clamp/negative diff). - ir_device.cpp + CLI ir-info now report the recovered protocol; skeleton errors changed from PROTOCOL_UNKNOWN to UNSUPPORTED; CMakeLists updated. - Full suite green: test_c_api, test_tcm_frame, test_tcm_device, test_device_model, test_ir_skeleton, test_ir_frame all EXIT=0; mag160c-cli ir-info shows the recovered protocol line. ## 2026-08-10 Windows Test Prep + USB Capture Tooling - Organized workspace: disassembly outputs moved to `analysis/disasm/`, legacy C++ exe artifacts moved to `analysis/legacy-cpp/build/`, temp files removed. - Hardware check: no VID 0x833C device currently attached (present USB devices: FFFF/FFFF, 0CF2/7750, 258A/010C, Logitech receiver). User will plug the camera in for Windows testing. - Installed USB capture tooling via winget: - USBPcap 1.5.4.0 (`desowin.USBPcap`) - driver + USBPcapCMD.exe - Wireshark 4.6.7 (`WiresharkFoundation.Wireshark`) - Reboot needed for the USBPcap driver to load. - Windows libusb-1.0 build support: - Found a 64-bit `libusb-1.0.dll` in TI CCS (`C:\ti\ccs2050\ccs\ccs_base\common\bin\`); openocd's copy is 32-bit (pei-i386), which crashed with 0xC000007B on a 64-bit exe. - Generated MinGW import library with `gendef` + `dlltool -k` (kill-at so symbols match the undecorated x64 calls) into `csdk/third_party/libusb/win64/` (libusb.h + libusb-1.0.dll + libusb-1.0.x64.a + original .def). - CMakeLists now falls back to this bundled libusb on Windows when pkg-config finds nothing, and copies the DLL next to the CLI. - Verified full libusb+threads build on Windows: - `csdk_cli_libusb.exe` runs; tcm/frame/temp tests pass; `ir-info` reports "no device with VID 0x833c" (libusb enumeration works); `ffc` returns NOT_OPEN cleanly. - Wrote `csdk/WINDOWS_TESTING.md`: build commands, hardware test flow, USBPcap capture procedure, and the pcap verification checklist against the recovered protocol (magic bytes per command/response/frame). ## 2026-08-10 Pure-C SDK Rewrite (csdk/) - User requested a C SDK faithful to the reverse-engineered protocol; the previous C++ implementation (fake `ir_open`, dynamic endpoint discovery, missing temperature math) was replaced. Old tree archived to `analysis/legacy-cpp/`. - New pure-C tree in `csdk/`: - `include/mag160c/mag160c.h` - C ABI: vendor error domain, all magic command/response codes, frame format constants, temperature pipeline API, TCM framing API. - `src/mag160c_error.c` - TLS last-error. - `src/mag160c_frame.c` - 0x1bb1b11b frame parse + streaming assembler (marker align, length check, trailing marker, 0x38+len consumption). - `src/mag160c_temp.c` - Calibration (per-pixel PWL w/ baseline), ConvertResponse2Temperature (auto-gain + saturation counting), T2E Q13 interpolation using the dumped tables. - `src/mag160c_tcm.c` - FTDICommand 0x7e framing; NOTE: checksum is additive mod-256 (0x7e+0x07=0x85), byte-exact vs. the old C++ tests. - `src/mag160c_ir.c` - libusb session: VID 0x833C, config 2, iface 0, EP 0x03/0x82 command channel, EP 0x81 frame reader thread, 50 ms then START, STOP, FFC; compiles in libusb/threads/no-libusb modes. - `src/mag160c_tables.h` - T2E + E2TAccQ10 tables generated from the ARM64 binary dump. - `tools/mag160c_cli.c` - ir-info / ffc / start / stop / tcm-rotate / tcm-light / frame-test / temp-test. - `tests/` - test_frame, test_temp, test_tcm, test_api. - Verification (mingw gcc, no-libusb mode): all 4 tests pass (ALL_CSDK_TESTS_PASSED); threads mode passes; libusb+threads mode compiles clean (no libusb .lib on this machine to link). - CLI output verified: `tcm-rotate 5` -> `7e 00 07 85 02 77 00 01 00 05 7f` (byte-exact), `tcm-light green blink` -> `7e 00 09 87 02 32 00 01 01 00 ff 00 35`, frame-test/temp-test pass, ir-info prints the recovered protocol. ## 2026-07-14 Workspace Organization - Organized generated and analysis files without deleting source or vendor SDK material. - Moved root `build_*.exe` binaries into `build-artifacts/`. - Moved `.reverse/` to `analysis/reverse-cache/`. - Moved `reverse_tools/` to `analysis/reverse_tools/`. - Moved root `【模块文档】MAG160CORE.pdf` to `vendor-docs/【模块文档】MAG160CORE.pdf`. - Left vendor SDK/app directories and active source directories in place: `IR_Camera_SDK-1.0.1/`, `app/`, `src/`, `include/`, `tests/`, `tools/`, and `docs/`. ## 2026-08-10 Live Hardware Protocol Verification (WinUSB/libusb-win32) User connected the actual device (Magnity Thermal Camera, VID 0x833C PID 0x0001, serial 160043865). Full protocol verified live against the official Windows demo (EloThermal.exe, which shows a real thermal image). ### Driver / libusb matrix (important) - Device ships without a driver; SDK's ThermoCube_stream.inf fails signature validation on Win10/11 (2012 cat). Zadig installed WinUSB, then libusb-win32 for the official demo (demo links libusb0.dll directly). - Working combo: **TI libusb-1.0.dll + libusb-win32 driver** (all commands/stream work; harmless "monotonic clock" warnings). Official libusb-1.0.27 MinGW build crashes on bulk transfer with the libusb-win32 driver; official DLL + WinUSB crashes in libusb_control_transfer; TI DLL + WinUSB fails FFC writes. ### Protocol verified on hardware (differs from Linux-SDK assumptions) - Commands are 4-byte packets (magic only); only FFC (0x6bb6b672) uses the 8-byte {magic, param} form. - Init: 66b(4B) -> 66c(4B) -> 66f(4B). Response 0x5bb5b55b from 66b carries the camera info (width@+0x10=160, height@+0x14=120, fps@+0x18=15). The 0x5bb5b55c block (from 66c) is a DIFFERENT layout and must not be parsed as info. - START: FFC(0) x2, sleep 300ms, then 73(4B). This switches the unit into live imaging mode (frames with type field 0/1). FFC(param=1) toggles type 1->0, FFC(param=0) toggles 0->1; the official demo alternates FFC every ~10 frames. - STOP: clear_halt(0x03/0x82) then 74(4B) works. - Frame stream: 28-byte header (marker 0x1bb1b11b, counter, len=0x9600, type, shutter) + 38400-byte payload + 28-byte tail (0x1bb1b11c trailer), at 15 fps. Verified: 4 s capture = 60 frames. - Thermal response: hand vs background delta ~1322 counts (whole-FOV hand) => roughly 147 counts/C with a plausible 34C vs 25C assumption. ### csdk fixes made from live testing - send_cmd4 for 66b/66c/66f/73/74; FFC stays 8-byte with param. - ir_open sequence 66b,66c,66f (was sending 670 in third slot). - FFC(0) x2 before START in ir_start. - Keep libusb_context alive for handle lifetime (was libusb_exit'ing it). - Only 0x5bb5b55b parsed as info (0x5bb5b55c corrupted width/height). - GUI live thermal viewer (thermal_viewer.c) shows real-time pseudo-color imagery; single-threaded read/render loop required (TI libusb DLL is unreliable with a reader thread + GUI thread). ### Artifacts - official demo traffic capture: analysis/captures/libusb0_trace.txt (26k lines: init, periodic FFC, 10k+ frames) via a libusb0.dll API shim. - ETW capture experiments (USB-UCX has no payload data; etl2pcapng is network-only) - kept as notes in csdk/WINDOWS_TESTING.md. ## 2026-08-10 Post-reboot USBPcap Verification - System rebooted; USBPcap driver now bound to the root hubs. Device is on \\\\.\\USBPcap4 (AMD USB 3.x hub on this machine). - Captured the csdk's own live traffic with USBPcap (no shim needed anymore): analysis/captures/csdk_verified.pcap. - Sequence verified identical to the official demo: 66b -> 66c -> 66f -> FFC(0) x2 -> START -> 61 frames @ 15 fps -> STOP. - csdk frame callback test: 60 frames in 4 s (type=1). All tests green. ## 2026-08-10 (evening) - Task 1: researched open-source thermal display pipelines (OpenThermal/libseek-thermal SeekCam.cpp, MLX90640 API, FLIR Lepton AGC/LUT). Key learnings: bad pixel = histogram peak-deviation threshold (thr = peak - (max - peak)) + temporal min/max, topological 4-neighbour fill; AGC = percentile stretch + ironbow LUT; diff AGC = adaptive mean-based span. - Task 2: rewrote tools/mag160c_demo2.c v3: Seek-style bad pixel detection + topological fill order applied to live frame and reference; adaptive diff AGC (span=2*mean|d|, deadband); ironbow LUT; two-point calibration buttons (Cal Cold 0C / Cal Hot 90C / Reset). - Task 3: decoded official FFC cadence from libusb0_trace.txt (26092 lines): FFC(1) after ~frame 10, then FFC(0) -> 9 frames -> FFC(1) -> variable gap (34..2680). Built mag160c_ffc_test.c with period 400 + gap 9: PASS 1400 type=0 frames / 95s / 15.1 fps / 0 stalls (twice). 44-frame period failed (fps drop + 17.7% type=1). - Task 4: added two-point linear calibration (mag160c_temp_calibrate_linear) to csdk + demo2 GUI buttons; physical measurement (ice water / boiling water) still TODO by user. - Task 5: new csdk module src/mag160c_display.c + include/mag160c/mag160c_display.h (badmap, AGC range/diff span, ironbow, FFC scheduler, linear cal); mag160c_ir.c now supports mag160c_ir_set_ffc_scheduler() ticking in the reader thread. test_display.c added (20/20 pass); all old tests still pass; CLI gained display-test/ffc-test/calibrate. - Hardware validation: csdk_stream_test.exe (threaded reader + scheduler) PASS 1030 type=0 frames / 14.7 fps; csdk_cli_libusb.exe ffc-test 1000 PASS. demo2 GUI verified rendering type=0 with bad-pixel red/blue down to ~0.02% and no saturated defects. - Evidence: analysis/demo2_v3_verified_20260810.png (GUI screenshot). ## 2026-08-10 (late night, third fix round) - User reported: FFC flash fixed but (1) startup noise image (red/yellow dots) persists and overlays the normal view, (2) ghosting still present. - Root cause: MOG permanent-freeze defect - reference collected before the first auto-FFC cycle baked startup noise in; frozen pixels (|d|>=60) could never heal, so noise + wrong reference persisted forever. - Fixes in mag160c_demo2.c: * first reference collection moved to right after startup FFC(1) settling (nread>=40, not >=150) * reference re-collected after EVERY FFC(1) (quiet-frame median, keeps bad map) instead of one-shot median rebase - any stale reference self-heals within one FFC cycle (~27s) * MOG self-healing: isolated foreground pixels (8-neighbor fg < 2) stuck for >=30 frames are reference errors (startup noise/bad pixel), reset to live value; contiguous objects never healed/absorbed - csdk: new mag160c_display_ref_track_heal() (w,h variant with fg counters); demo2 uses it directly. test_display now 30/30. - Verified on hardware: startup noise 0.12% -> 0.10% over 2157 frames (5+ FFC cycles), no growth, no overlay; red content all real scene blobs; no FFC flash (red stable, blue 0.01%); stream never stalls. - Evidence: analysis/demo2_v4_selfheal_verified_20260810.png ## 2026-08-10 (late night, fourth fix round - root cause found) - User reported startup noise image and ghosting STILL present after the MOG self-heal fix. Started a data-driven investigation instead of more reference-model tweaks. - Captured 60 real type=0 frames (tools/mag160c_frame_dump.c) and analyzed: * temporal noise std ~248 counts per pixel (99.9% of pixels) -> diff mode with adaptive span ~400 amplifies this into visible red/blue speckle = the "startup noise image" * fixed sensor mura: row profile span 15594 (row1 constant +5000), col span 8714, FFT shows ~10% energy on row/col axes * mura drifts ~350 counts per session and jumps after FFC -> reference diff shows old pattern = ghosting * flat-field correction live-(ref-mean(ref)) reduces mura std 3557->379 - Decided to abandon the scene-diff approach (which caused both problems) and adopt the official pipeline: raw -> NUC flat-field correction -> absolute temperature display (median-centered window + ironbow) + 3x3 median smoothing (cuts temporal noise ~3x). - csdk: new mag160c_display_nuc(); reference capture now rejects frames where >5% pixels deviate from the running average (persistent objects never enter the flat field) - the anti-ghost guarantee. diff mode kept as optional. - Verified on hardware: speckle residual 2.2-2.7 (smooth), pure blue 0%, red content all real scene blobs (1-1.4%), 5+ FFC cycles with no flash and no pattern growth, stream 2268+ frames. csdk tests 32/32. - Evidence: analysis/demo2_v5_nuc_verified_20260810.png