From a154e715f65694e5418aee59feb41570500f173b Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin <6576495+widgetii@users.noreply.github.com> Date: Sun, 3 May 2026 17:26:18 +0300 Subject: [PATCH] Detect runtime mode switches in segmenter, emit per-mode functions Closes the fourth real gap from #145's plan-vs-shipped audit, with one caveat documented in the docs and tested empirically below. trace_segment.py: new find_mode_switches() walks the trace after init_end watching for `0x100=0 ... 0x100=1` cycles. Each cycle becomes a mode_switch_N phase. The runtime/post_init anchors move to the last 0x100=1, so traces without a mode switch are unchanged. trace_to_driver.py: emits one `_set_mode_N` function per mode-switch phase, same shape as `_linear_init`. test_pipeline.sh: synthetic fixture extended with a mode switch (0x100=0 / VMAX-rewrite / 0x100=1). CI asserts `_set_mode_1` is emitted in the generated C and that it still passes gcc -Wall -Wextra and the self-diff. Empirical caveat: capture-side, mode switches require a streamer that supports runtime sensor reconfiguration. Majestic does not (config goes through .ini files + restart, each mode is a separate cold-init capture). Sofia does, via the DVR-IP protocol; python-dvr exposes the knobs. But whether a given knob causes a sensor-side reconfigure is sensor-specific - Sofia's BroadTrends path lands in software-side gain on most sensors. On SC2315E specifically, toggling AutoGain 0->1->0 while ipctool trace was watching produced *zero* additional 0x100 cycles - the sensor stays in linear mode. Sofia's supported-sensor list confirms `SC2315_WDR` is a separate entry from `SC2315E`, so no WDR firmware exists for that combo. The segmenter and generator are validated end-to-end on the synthetic fixture; live validation with a multi-mode sensor is deferred to whoever has hardware where Sofia drives a sensor with a `_WDR` variant. Docs: new "Capturing mode switches" section in sensor-driver-extraction.md walks through Majestic vs Sofia, the python-dvr API, the empirical SC2315E finding, and the heuristic's known blind spot (group-hold based hot swaps that don't toggle 0x100). Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/sensor-driver-extraction.md | 49 +++++++++++++++++++++++++++++ tools/test_pipeline.sh | 6 ++++ tools/trace_segment.py | 54 ++++++++++++++++++++++++++++++-- tools/trace_to_driver.py | 15 +++++++++ 4 files changed, 121 insertions(+), 3 deletions(-) diff --git a/docs/sensor-driver-extraction.md b/docs/sensor-driver-extraction.md index 601df6e..fdb4bba 100644 --- a/docs/sensor-driver-extraction.md +++ b/docs/sensor-driver-extraction.md @@ -393,6 +393,55 @@ python3 tools/trace_diff.py \ --ref-scope sc2315e_linear_1080P30_init ``` +## Capturing mode switches + +A "mode switch" here is a runtime sensor reconfiguration — switching +1080p25 to 720p, or linear to WDR — without a full streamer restart. + +The capture-side mechanism is streamer-specific. **OpenIPC Majestic +does not support runtime mode switching**: configuration changes go +through `/etc/sensors/*.ini` files and require a streamer restart, so +each mode is a separate cold-init capture. **XiongMai Sofia does** +support several runtime knobs via the DVR-IP TCP protocol on port +34567; the [python-dvr](https://github.com/OpenIPC/python-dvr) client +exposes them. Example, toggling Sofia's `BroadTrends.AutoGain` knob: + +```python +from dvrip import DVRIPCam +cam = DVRIPCam('10.216.128.106', user='admin', password='') +cam.login() +cam.set_info("Camera.ParamEx.[0]", + {"BroadTrends": {"AutoGain": 1, "Gain": 50}}) +``` + +Whether a given knob actually causes a sensor-side reconfigure is +**sensor-specific** — Sofia's BroadTrends path lands in software-side +gain control on most sensors and only triggers a sensor-side WDR-mode +change on sensors whose firmware has a separate WDR variant. As a +data point, when toggling `AutoGain` 0→1→0 on the SC2315E camera at +`10.216.128.106` while `ipctool trace` was watching, the trace shows +**zero** additional `0x100` cycles after init — the sensor stays in +linear mode regardless. Sofia's supported-sensor list confirms this: +`SC2315_WDR` is a separate entry from `SC2315E`, so no WDR firmware +exists for our test SoC. To exercise mode-switch capture end-to-end, +use a sensor that Sofia knows in `_WDR` form (SC2315, IMX307, etc.). + +### Segmenter heuristic + +`trace_segment.py` detects mode switches by watching for a `0x100=0` +write **after** init has completed (`init_end`), paired with the next +`0x100=1` to form a `mode_switch_N` phase. Multiple cycles produce +`mode_switch_1`, `mode_switch_2`, etc. The post-init AE prime and +runtime steady state then anchor on the *last* `0x100=1`, so a trace +with no mode switch is identical to before. + +`trace_to_driver.py` emits one `_set_mode_N` function per +mode-switch phase, in the same shape as `_linear_init`. + +If a sensor hot-swaps modes via a group-hold (e.g. `0x3812=0x00 ... +0x3812=0x30` block) without toggling `0x100`, this heuristic misses +the boundary — extend the segmenter when you hit such a sensor. + ## Stage 4 — Live-reading the AE state with `ipctool sensor monitor` `ipctool sensor` is a built-in subcommand (separate from `trace`) that diff --git a/tools/test_pipeline.sh b/tools/test_pipeline.sh index 80ad65b..5d24655 100755 --- a/tools/test_pipeline.sh +++ b/tools/test_pipeline.sh @@ -31,6 +31,10 @@ sensor_write_register(0x3039, 0xa6); sensor_write_register(0x320e, 0x4); sensor_write_register(0x100, 0x1); sensor_write_register(0x3e02, 0x80); +sensor_write_register(0x100, 0x0); +sensor_write_register(0x320e, 0x8); +sensor_write_register(0x320c, 0x10); +sensor_write_register(0x100, 0x1); sensor_write_register(0x5781, 0x60); sensor_write_register(0x5781, 0x60); sensor_write_register(0x5781, 0x60); @@ -59,6 +63,8 @@ grep -q '^void testsensor_linear_init' "$tmp/driver.c" \ || { echo "linear_init function not emitted"; exit 1; } grep -q '^void testsensor_ae_step' "$tmp/driver.c" \ || { echo "ae_step skeleton not emitted"; exit 1; } +grep -q '^void testsensor_set_mode_1' "$tmp/driver.c" \ + || { echo "set_mode_1 (mode-switch) function not emitted"; exit 1; } grep -q '^combo_dev_attr_t SENSOR_ATTR' "$tmp/driver.c" \ || { echo "MIPI struct not emitted at file scope"; exit 1; } grep -q '^#if 0' "$tmp/driver.c" \ diff --git a/tools/trace_segment.py b/tools/trace_segment.py index 26f318c..a081a46 100644 --- a/tools/trace_segment.py +++ b/tools/trace_segment.py @@ -169,6 +169,43 @@ def find_runtime_start(events, init_end): return None +def find_mode_switches(events, init_end): + """Find mode-switch boundaries after init_end. + + A mode switch on a HiSilicon-style sensor cycles 0x100 (stream control): + write 0x100=0 to halt, reconfigure mode-specific registers, write + 0x100=1 to resume. Each such cycle is a `mode_switch_N` phase. + + Sensors that hot-swap modes via group-hold (e.g. 0x3812 toggling + 0x00 -> writes -> 0x30) are not detected by this heuristic. Add a + parallel detector if the runtime hot-set on that sensor turns out + to mask a real mode change. + + Returns a list of (start, end) tuples, both inclusive, in trace order. + """ + if init_end is None: + return [] + switches = [] + i = init_end + 1 + while i < len(events): + k, p = events[i] + if k == "write" and p["reg"] == 0x100 and p["val"] == 0: + start = i + end = None + for j in range(start + 1, len(events)): + k2, p2 = events[j] + if k2 == "write" and p2["reg"] == 0x100 and p2["val"] == 1: + end = j + break + if end is None: + break # incomplete cycle at end of trace; ignore + switches.append((start, end)) + i = end + 1 + else: + i += 1 + return switches + + def slice_events(events, start, end): return events[start : end + 1] if start is not None and end is not None else [] @@ -195,7 +232,11 @@ def main(): # Phase boundaries. init_s, init_e = find_init_bounds(events) - runtime_s = find_runtime_start(events, init_e) + mode_switches = find_mode_switches(events, init_e) + # post_init and runtime live AFTER any mode switches, so anchor on the + # last 0x100=1 we saw (init_end if no switches, last switch end otherwise). + last_streamon = mode_switches[-1][1] if mode_switches else init_e + runtime_s = find_runtime_start(events, last_streamon) phases = {} if init_s is None: @@ -203,11 +244,18 @@ def main(): else: phases["pre_sensor"] = serialize(events[:init_s]) phases["init"] = serialize(events[init_s : init_e + 1]) + # Each mode switch becomes its own phase. The window between + # init_end and the first switch (and between switches) is steady + # state for the previous mode; merge it into the prior phase's + # tail so the mode_switch_N phase strictly contains the cycle. + for n, (s, e) in enumerate(mode_switches, 1): + phases[f"mode_switch_{n}"] = serialize(events[s : e + 1]) + post_init_start = last_streamon + 1 if runtime_s is not None: - phases["post_init"] = serialize(events[init_e + 1 : runtime_s]) + phases["post_init"] = serialize(events[post_init_start:runtime_s]) phases["runtime"] = serialize(events[runtime_s:]) else: - phases["post_init"] = serialize(events[init_e + 1 :]) + phases["post_init"] = serialize(events[post_init_start:]) summary = {phase: len(events) for phase, events in phases.items()} out_path = args.out or args.input + ".segments.json" diff --git a/tools/trace_to_driver.py b/tools/trace_to_driver.py index 2aa39cb..5b26f3f 100644 --- a/tools/trace_to_driver.py +++ b/tools/trace_to_driver.py @@ -242,6 +242,21 @@ def main(): body=emit_phase(init, indent=" "), ) ) + # Each runtime mode switch (0x100=0 ... 0x100=1 cycle after init) gets + # its own function. Sensor that hot-swaps without toggling 0x100 (e.g. + # via group-hold) won't surface here; the segmenter doesn't detect that. + for key in sorted(phases): + if not key.startswith("mode_switch_"): + continue + n = key.split("_")[-1] + parts.append( + FN_TEMPLATE.format( + sensor=args.sensor, + suffix=f"set_mode_{n}", + body=emit_phase(phases[key], indent=" "), + ) + ) + if post_init: # Kept separate from init: these are AE/exposure prime writes that # would otherwise overwrite init values when the diff merges them.