Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions docs/sensor-driver-extraction.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<sensor>_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
Expand Down
6 changes: 6 additions & 0 deletions tools/test_pipeline.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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" \
Expand Down
54 changes: 51 additions & 3 deletions tools/trace_segment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 []

Expand All @@ -195,19 +232,30 @@ 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:
phases["pre_sensor"] = serialize(events)
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"
Expand Down
15 changes: 15 additions & 0 deletions tools/trace_to_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading