Skip to content

dvrip_twin: X1.0, zoom-out levels, and the board's own focus before the pass - #39

Merged
widgetii merged 6 commits into
masterfrom
dvrip-twin-x1
Oct 1, 2026
Merged

widgetii merged 6 commits into
masterfrom
dvrip-twin-x1

Conversation

@widgetii

@widgetii widgetii commented Oct 1, 2026

Copy link
Copy Markdown
Member

Extends uart-bridge/scripts/dvrip_twin.py (#38) with X1.0, zoom-out levels, and a measure of where each lens board's own tracking leaves focus.

What's new

  • X1.0. The wide stop itself, reached the way a zoom-out reaches it: settled at X2.0, then one zoom-out into the stop.
  • Zoom-out levels, --levels out-X4.0 out-X3.0 out-X2.0 out-X1.5 out-X1.0. Each settles at the tele end, then zooms out for the full range less the level's zoom-in hold. The default run is still the zoom-in levels.
  • The board's own focus, at +2s. For each camera, the picture 1.5–2.8 s after the stop, before majestic-af's pass starts (3 s), as a fraction of that camera's best.
  • Docs. README, TWIN.md, and two new pitfalls:
    • the OpenIPC reference can read over 100 %, because a 100 ms /ptz pulse moves this lens irregularly;
    • the harness's centre box is not the ISP's focus statistic.
  • PROTOCOL.md. A zoom-out leaves the XM board's focus well off.

What it found

The stock firmware stays blurred after a zoom-out, at 0–7 % of its best at X1.0–X2.8. Full-size centre sharpness:

ratio after a zoom-in after a zoom-out
~X2 2764 27
~X3 4987 288

On OpenIPC the board's own focus falls the same way, and majestic-af then fixes it.

The cause is the zoom gear's slack: the board sets focus from its zoom count, and after a zoom-out the lens falls short of that count. majestic-af #18 now ends every zoom-out with a short zoom-in. The board's own focus 2 s after a zoom-out, as a fraction of the settled picture:

zoom-out to before #18 after #18
X2.0 0.40 0.80–0.89
X1.5 0.34 0.62–0.98

Captures

Tests

uv run pytest: 70 passed. New tests cover the zoom-out moves and the board-only window.

X1.0 is the wide stop, which every zoom-in level starts from, so no zoom-in
ever lands there and it was never measured, though it is where the old
majestic-af model failed ("X1.0 can't focus", on the degraded lens). A level is
now the moves that reach it from anywhere, the last one recorded: a zoom-in
level is the wide stop and its zoom-in, X1.0 is settling at X2.0 and then one
held zoom-out into the stop. The reference sweeps reach each level by the same
moves (by DVRIP on OpenIPC, by timed pulses on the stock board).
out-X4.0 .. out-X1.0 settle at the tele end, then zoom out for the full range
less the level's zoom-in hold (out-X1.0 into the wide stop). A zoom-out drives
focus the other way through the board's tracking, and lands differently.

Each level also records the picture 1.5-2.8 s after the stop, before
majestic-af's pass starts: where the lens board's own tracking left focus,
on both cameras, reported as a fraction of the best.
…PROTOCOL: zoom-out focus

- Tests for the out-X* moves and the board-only window.
- README/TWIN.md: the zoom-out levels, the 'at +2s' columns, and two
  pitfalls: the OpenIPC reference reading over 100 % (irregular /ptz pulses)
  and the centre box disagreeing with the ISP's focus statistic.
- PROTOCOL.md: a zoom-out leaves the board's focus well off (zoom gear
  slack); a 150-200 ms zoom-in after it fixes that; the stock firmware does
  not, and stays at 0-7 % of best.
- captures: the in/out runs before and after majestic-af #18.
@widgetii

widgetii commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Two zoom-out levels lose stock focus scores ✓ Resolved
Description
reference_stock() replays the zoom-out holds as timed board pulses, but the measured run uses
DVRIP holds that move the stock lens for a different duration. In the supplied capture, out-X2.0
and out-X4.0 each land 0.2 zoom apart from their stock references, so the 0.1 comparison gate
discards both percentage-of-best scores despite valid focus sweeps.
Code

uart-bridge/scripts/dvrip_twin.py[R256-257]

+        for cmd, hold in moves(lv):
+            reports = board.pulse("ref", A.ZOOM_OUT if cmd == "ZoomWide" else A.ZOOM_IN, hold) or reports
Evidence
The measured move uses DVRIP, whereas the added stock reference uses timed pulses; the script
documents that stock DVRIP holds run longer than requested. The supplied capture records stock
run/reference zooms of 1.8/2.0 for out-X2.0 and 3.7/3.9 for out-X4.0, with ref_zoom_mismatch
and no of_best for either; the comparison code rejects differences greater than 0.1.

uart-bridge/scripts/dvrip_twin.py[39-46]
uart-bridge/scripts/dvrip_twin.py[199-203]
uart-bridge/scripts/dvrip_twin.py[253-260]
uart-bridge/scripts/dvrip_twin.py[501-507]
xm-uart/captures/dvrip-twin-inout-before.json[1-1]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Timed stock-board pulses reach different zoom positions from the DVRIP moves being measured, leaving two new zoom-out levels without stock percentage-of-best scores.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[253-260]
- uart-bridge/scripts/dvrip_twin.py[501-507]
## Recommended Fix
Pass each measured stock zoom to the reference sweep and adjust the injected zoom-out move until the board reports that position, preserving its zoom-out approach. Keep the zoom-mismatch guard rather than comparing sharpness at different magnifications.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Custom zoom runs lose their chosen order ✓ Resolved
Description
main() builds levels by iterating the predefined level dictionary rather than the parsed
--levels arguments. A user requesting a different sequence, or repeating a level for comparison,
gets a reordered or deduplicated run instead.
Code

uart-bridge/scripts/dvrip_twin.py[419]

+    levels = [lv for lv in LEVELS if lv in a.levels]
Evidence
Argument parsing accepts an ordered list, but the new comprehension iterates LEVELS; the
subsequent run iterates that result, so caller order is discarded and membership testing removes
repetitions.

uart-bridge/scripts/dvrip_twin.py[411-419]
uart-bridge/scripts/dvrip_twin.py[439-441]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`main()` silently reorders and deduplicates `--levels`, changing the requested experiment.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[411-419]
## Recommended Fix
Use the parsed `a.levels` sequence directly when running and recording levels. If repeated levels are supported, store their results without overwriting an earlier run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Late focus changes skew the two-second score ✓ Resolved
Description
board_of_best scales the recorded board-only and settled sharpness values using final_sharp, a
separate live measurement taken after recording and decoding finish. If autofocus continues past the
30-second video window and completes during that later work, the reported two-second fraction
combines pictures from different focus states.
Code

uart-bridge/scripts/dvrip_twin.py[R509-511]

+                bo, st = r[n].get("board_only"), r[n].get("settled")
+                if bo and st:
+                    r[n]["board_of_best"] = round(final / best * bo / st, 2)
Evidence
Autofocus waiting is capped at the remaining video duration. Recording.finish() waits for ffmpeg
and decodes the file before level_run() obtains final_sharp from a fresh RTSP sharpness sample;
the added calculation then combines that value with two recording-derived values.

uart-bridge/scripts/dvrip_twin.py[203-214]
uart-bridge/scripts/xm_tracking.py[139-145]
uart-bridge/scripts/dvrip_twin.py[507-511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new board-only fraction can use a post-recording focus state to scale values measured inside the recording.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[203-214]
- uart-bridge/scripts/dvrip_twin.py[507-511]
## Recommended Fix
Ensure the full-size scaling measurement corresponds to the settled portion of the recorded timeline, or omit the normalized board-only score when autofocus has not finished within that recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View medium (4)
4. Custom zoom runs lose their chosen order 🐞 Bug ≡ Correctness
Description
main() builds levels by iterating the predefined level dictionary rather than the parsed
--levels arguments. A user requesting a different sequence, or repeating a level for comparison,
gets a reordered or deduplicated run instead.
Code

uart-bridge/scripts/dvrip_twin.py[419]

+    levels = [lv for lv in LEVELS if lv in a.levels]
Evidence
Argument parsing accepts an ordered list, but the new comprehension iterates LEVELS; the
subsequent run iterates that result, so caller order is discarded and membership testing removes
repetitions.

uart-bridge/scripts/dvrip_twin.py[411-419]
uart-bridge/scripts/dvrip_twin.py[439-441]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`main()` silently reorders and deduplicates `--levels`, changing the requested experiment.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[411-419]
## Recommended Fix
Use the parsed `a.levels` sequence directly when running and recording levels. If repeated levels are supported, store their results without overwriting an earlier run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Late focus changes skew the two-second score ✓ Resolved
Description
board_of_best scales the recorded board-only and settled sharpness values using final_sharp, a
separate live measurement taken after recording and decoding finish. If autofocus continues past the
30-second video window and completes during that later work, the reported two-second fraction
combines pictures from different focus states.
Code

uart-bridge/scripts/dvrip_twin.py[R509-511]

+                bo, st = r[n].get("board_only"), r[n].get("settled")
+                if bo and st:
+                    r[n]["board_of_best"] = round(final / best * bo / st, 2)
Evidence
Autofocus waiting is capped at the remaining video duration. Recording.finish() waits for ffmpeg
and decodes the file before level_run() obtains final_sharp from a fresh RTSP sharpness sample;
the added calculation then combines that value with two recording-derived values.

uart-bridge/scripts/dvrip_twin.py[203-214]
uart-bridge/scripts/xm_tracking.py[139-145]
uart-bridge/scripts/dvrip_twin.py[507-511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new board-only fraction can use a post-recording focus state to scale values measured inside the recording.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[203-214]
- uart-bridge/scripts/dvrip_twin.py[507-511]
## Recommended Fix
Ensure the full-size scaling measurement corresponds to the settled portion of the recorded timeline, or omit the normalized board-only score when autofocus has not finished within that recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Custom zoom runs lose their chosen order 🐞 Bug ≡ Correctness
Description
main() builds levels by iterating the predefined level dictionary rather than the parsed
--levels arguments. A user requesting a different sequence, or repeating a level for comparison,
gets a reordered or deduplicated run instead.
Code

uart-bridge/scripts/dvrip_twin.py[419]

+    levels = [lv for lv in LEVELS if lv in a.levels]
Evidence
Argument parsing accepts an ordered list, but the new comprehension iterates LEVELS; the
subsequent run iterates that result, so caller order is discarded and membership testing removes
repetitions.

uart-bridge/scripts/dvrip_twin.py[411-419]
uart-bridge/scripts/dvrip_twin.py[439-441]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`main()` silently reorders and deduplicates `--levels`, changing the requested experiment.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[411-419]
## Recommended Fix
Use the parsed `a.levels` sequence directly when running and recording levels. If repeated levels are supported, store their results without overwriting an earlier run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Late focus changes skew the two-second score ✓ Resolved
Description
board_of_best scales the recorded board-only and settled sharpness values using final_sharp, a
separate live measurement taken after recording and decoding finish. If autofocus continues past the
30-second video window and completes during that later work, the reported two-second fraction
combines pictures from different focus states.
Code

uart-bridge/scripts/dvrip_twin.py[R509-511]

+                bo, st = r[n].get("board_only"), r[n].get("settled")
+                if bo and st:
+                    r[n]["board_of_best"] = round(final / best * bo / st, 2)
Evidence
Autofocus waiting is capped at the remaining video duration. Recording.finish() waits for ffmpeg
and decodes the file before level_run() obtains final_sharp from a fresh RTSP sharpness sample;
the added calculation then combines that value with two recording-derived values.

uart-bridge/scripts/dvrip_twin.py[203-214]
uart-bridge/scripts/xm_tracking.py[139-145]
uart-bridge/scripts/dvrip_twin.py[507-511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new board-only fraction can use a post-recording focus state to scale values measured inside the recording.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[203-214]
- uart-bridge/scripts/dvrip_twin.py[507-511]
## Recommended Fix
Ensure the full-size scaling measurement corresponds to the settled portion of the recorded timeline, or omit the normalized board-only score when autofocus has not finished within that recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Findings visible per group, which tucks the rest behind a View link

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Measure zoom-out focus and the wide stop in the DVRIP twin harness

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add X1.0 and zoom-out levels to compare focus after approaching the same ratio from either
 direction.
• Measure each board’s focus before OpenIPC autofocus, with optional best-focus references.
• Add tests, capture data, and documentation of zoom-out blur and measurement limitations.
Diagram

graph TD
  L["Selected Levels"] --> M["Move Sequences"] --> S["Stock Camera"] --> T["Sharpness Timelines"] --> R["Comparison Report"]
  M --> O["OpenIPC Camera"] --> T
  S --> F["Focus References"] --> R
  O --> F
Loading
High-Level Assessment

Keep the shared move definition for recorded runs and reference sweeps: it makes directional comparisons meaningful without duplicating lens-position logic. Direct ISP focus statistics were considered but would not provide the same video-based measure on both cameras; this PR appropriately leaves the majestic-af zoom-out correction outside the harness.

Files changed (7) +120 / -26

Enhancement (1) +79 / -24
dvrip_twin.pyRun directional zoom levels and report board-only focus +79/-24

Run directional zoom levels and report board-only focus

• Adds X1.0 and out-X levels through shared preparation and final-move sequences used by both recorded runs and reference sweeps. Extracts median sharpness 1.5–2.8 seconds after each stop and, when references exist, reports that landing as a fraction of the camera’s best focus.

uart-bridge/scripts/dvrip_twin.py

Tests (3) +23 / -0
test_dvrip_twin.pyTest directional moves and the board-only window +21/-0

Test directional moves and the board-only window

• Checks default and zoom-out level definitions, representative move sequences, and median sharpness selection before the autofocus pass, including an empty window.

uart-bridge/tests/test_dvrip_twin.py

dvrip-twin-inout-after.jsonCapture directional results with majestic-af #18 +1/-0

Capture directional results with majestic-af #18

• Adds stock and OpenIPC zoom-in and zoom-out recordings, sharpness timelines, and board-only measurements after the external majestic-af zoom-out correction. This run does not include reference sweeps.

xm-uart/captures/dvrip-twin-inout-after.json

dvrip-twin-inout-before.jsonCapture directional baselines with majestic-af #17 +1/-0

Capture directional baselines with majestic-af #17

• Adds recordings and per-level focus sweeps for both cameras before the external correction. The results document severe stock blur after zoom-out and include reference-mismatch flags where a stock sweep reached a different zoom ratio.

xm-uart/captures/dvrip-twin-inout-before.json

Documentation (3) +18 / -2
README.mdSummarize directional levels and pre-autofocus measurement +5/-2

Summarize directional levels and pre-autofocus measurement

• Explains that the twin harness can zoom out from the tele end and records each lens board’s focus before OpenIPC’s autofocus pass.

uart-bridge/README.md

TWIN.mdDocument zoom-out runs and sharpness pitfalls +8/-0

Document zoom-out runs and sharpness pitfalls

• Adds the out-X level command and explains the at +2s columns. Notes that irregular OpenIPC focus pulses can make a reference read over 100%, and that centre-box video sharpness differs from the ISP statistic used by majestic-af.

uart-bridge/TWIN.md

PROTOCOL.mdRecord verified zoom-out focus behavior +5/-0

Record verified zoom-out focus behavior

• Documents the observed focus loss after zoom-out, the short zoom-in correction observed on OpenIPC, and the stock firmware’s persistent blur. Adds the initial and before/after twin-run captures to the capture index.

xm-uart/PROTOCOL.md

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Custom zoom runs lose their chosen order 🐞 Bug ≡ Correctness
Description
main() builds levels by iterating the predefined level dictionary rather than the parsed
--levels arguments. A user requesting a different sequence, or repeating a level for comparison,
gets a reordered or deduplicated run instead.
Code

uart-bridge/scripts/dvrip_twin.py[419]

+    levels = [lv for lv in LEVELS if lv in a.levels]
Evidence
Argument parsing accepts an ordered list, but the new comprehension iterates LEVELS; the
subsequent run iterates that result, so caller order is discarded and membership testing removes
repetitions.

uart-bridge/scripts/dvrip_twin.py[411-419]
uart-bridge/scripts/dvrip_twin.py[439-441]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`main()` silently reorders and deduplicates `--levels`, changing the requested experiment.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[411-419]
## Recommended Fix
Use the parsed `a.levels` sequence directly when running and recording levels. If repeated levels are supported, store their results without overwriting an earlier run.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Late focus changes skew the two-second score 🐞 Bug ≡ Correctness
Description
board_of_best scales the recorded board-only and settled sharpness values using final_sharp, a
separate live measurement taken after recording and decoding finish. If autofocus continues past the
30-second video window and completes during that later work, the reported two-second fraction
combines pictures from different focus states.
Code

uart-bridge/scripts/dvrip_twin.py[R509-511]

+                bo, st = r[n].get("board_only"), r[n].get("settled")
+                if bo and st:
+                    r[n]["board_of_best"] = round(final / best * bo / st, 2)
Evidence
Autofocus waiting is capped at the remaining video duration. Recording.finish() waits for ffmpeg
and decodes the file before level_run() obtains final_sharp from a fresh RTSP sharpness sample;
the added calculation then combines that value with two recording-derived values.

uart-bridge/scripts/dvrip_twin.py[203-214]
uart-bridge/scripts/xm_tracking.py[139-145]
uart-bridge/scripts/dvrip_twin.py[507-511]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new board-only fraction can use a post-recording focus state to scale values measured inside the recording.
## Fix Focus Areas
- uart-bridge/scripts/dvrip_twin.py[203-214]
- uart-bridge/scripts/dvrip_twin.py[507-511]
## Recommended Fix
Ensure the full-size scaling measurement corresponds to the settled portion of the recorded timeline, or omit the normalized board-only score when autofocus has not finished within that recording.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: ⚖️ Balanced: Downgraded extended -> standard: change is below the extended eligibility bar (hunks 18/18, lines 146/200; both must reach the floor). Router rationale: This changes timing-sensitive camera-control logic across multiple paths, adds new level semantics and reference handling, and introduces several independent data/reporting behaviors with substantial potential for subtle defects.

Grey Divider

Tip of the day
💡 Did you know, you can keep summaries lean with Findings visible per group, which tucks the rest behind a View link

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread uart-bridge/scripts/dvrip_twin.py Outdated
Comment thread uart-bridge/scripts/dvrip_twin.py
…he pictures match

- --levels runs in the order given (a level named twice runs once: the
  report is keyed by level).
- board_of_best only where the video and the full-size final_sharp show the
  same focus: no camera-side autofocus, or its pass finished in the video.
@widgetii

widgetii commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

/review

Comment thread uart-bridge/scripts/dvrip_twin.py Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 3b448aa

Timed pulses on the wire move the stock lens less than the run's DVRIP holds
(held 0.10-0.15 s longer), so two zoom-out references stopped 0.2 short of
the run's zoom and the zoom-match guard dropped their % of best. The
reference now nudges on in 0.1 s pulses, the same way, until the board
reports the zoom the run reached (at most 4). The guard stays.
@widgetii

widgetii commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

/review

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 2eda2d1

@widgetii
widgetii merged commit a0e39e9 into master Oct 1, 2026
3 checks passed
@widgetii
widgetii deleted the dvrip-twin-x1 branch October 1, 2026 19:22
widgetii added a commit that referenced this pull request Oct 2, 2026
…irmware's ONVIF PTZ (#41)

The twin harness now drives both cameras over ONVIF as well as DVRIP.
PROTOCOL.md records what the stock firmware does with ONVIF PTZ and
Imaging calls on the lens wire. This is the ONVIF counterpart of
#38/#39. The majestic side is widgetii/majestic#953 and the conformance
tests are OpenIPC/onvif-tt#12, both merged.

### uart-bridge
- **`scripts/onvif_ptz.py`** (new) offers the same `step`/`mark`/`close`
as the DVRIP controller.
- ZoomTile/ZoomWide go out as PTZ ContinuousMove at ±1 and then Stop.
FocusNear/FocusFar go out as Imaging continuous Move at ±1 and then
Imaging Stop.
- SOAP goes through onvif-tt's `DUT`, imported from a checkout the way
python-dvr is.
- The hold is timed from the request, not from the answer. Stock can
answer more than a second after its frame is already on the wire; timed
from the answer, a 2.8 s hold zoomed for 4.07 s.
- **`dvrip_twin.py`** gets `--transport dvrip|onvif`, `--onvif-tt`,
`--stock-onvif-port` (8899) and `--openipc-onvif-port` (80, credentials
from `--openipc-http`). The report records the transport; the verdict
and table are unchanged.
- **Tests:** a fake DUT checks that each step lands on the right
service, that the hold is timed from the request, and that an unknown
command is refused (74 passed).
- **TWIN.md:** new section *7. Driving both cameras over ONVIF*.

### xm-uart/PROTOCOL.md: *Driven over ONVIF (stock)*
Measured with the board's UART captured
(`captures/onvif-stock.jsonl.gz`):
- **PTZ ContinuousMove:** zoom ± sends zoom in/out once. Pan/tilt sends
right/left/up/down at 0x3F. Speed and Timeout are ignored, and the move
runs until Stop. Zoom and pan together send the pan frame, then the zoom
frame.
- **PTZ Stop** stops the lens whatever its flags. A zero velocity sends
nothing.
- **Imaging Move:** + is focus near (cmd2 0x80), − is far.
- **RelativeMove, GotoHome and GotoPreset** move nothing.
- **Timing:** frames go out 0.45–0.8 s after the request.

The section also lists the stock firmware's ONVIF quirks and how
majestic deliberately differs.

### Lockstep result: `captures/onvif-twin-ref.json`
Command: `dvrip_twin.py --transport onvif --reference`, all levels.

| Level | Stock zoom | OpenIPC zoom | OpenIPC AF done |
|---|---|---|---|
| X1.0 | 1.0 | 1.0 | 9.2 s |
| X2.0 | 2.2 | 2.2 | 6.6 s |
| X3.0 | 3.1 | 3.1 | 8.1 s |
| X4.0 | 4.0 | 4.0 | 8.1 s |
| X5.0 | 5.0 | 5.0 | 6.6 s |
| out-X4.0 | 3.9 | 3.8 | 8.1 s |
| out-X3.0 | 2.9 | 2.9 | 8.6 s |
| out-X2.0 | 2.0 | 2.0 | 7.6 s |
| out-X1.5 | 1.4 | 1.4 | 8.7 s |
| out-X1.0 | 1.0 | 1.0 | 9.7 s |

- **Zoom:** the two cameras land within 0.1 at every level, with the
same commands over ONVIF.
- **Focus nudge:** a manual focus nudge (an Imaging Move) moved both
lenses, and neither was followed by a refocus.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant