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.