Skip to content

feat: camera control — read and adjust dashcam settings over Wi-Fi#21

Merged
RobXYZ merged 3 commits into
RobXYZ:mainfrom
droomurray:feat/camera-control
Jun 18, 2026
Merged

feat: camera control — read and adjust dashcam settings over Wi-Fi#21
RobXYZ merged 3 commits into
RobXYZ:mainfrom
droomurray:feat/camera-control

Conversation

@droomurray

Copy link
Copy Markdown
Contributor

Camera control — read and adjust dashcam settings over Wi-Fi

Adds a Camera tab and /api/camera/* endpoints to read the dashcam's current settings and safely change them over the undocumented Novatek netapp HTTP interface.

What it does — reads settings (cmd=3014) decoded against a derived per-model command map; allow-listed writes validated against the camera's option list and read back to verify; on/off toggles + drop-downs + a recording pill; settings the camera refuses are read-only with a reason (resolution over Wi-Fi, exposure, lenses not attached), and lens-dependent ones re-enable automatically when the lens is connected.

Safety — hard denylist of destructive ids (format/factory-reset/firmware/delete/reboot/restart-wifi/SSD), refused before any request is built and never shown; gentle single-request transport; auto pause/resume recording for record-gated settings.

Datacommand_map.json reformats only the factual API data (ids, keys, option enums) from the VIOFO app's device-cmd-manager.db; the .db itself is not redistributed (scripts/build_command_map.py regenerates it). All 29 app models mapped; A329S validated on hardware, others untested.

Teststests/test_control.py, 11 offline unit tests.

Adds a Camera tab and /api/camera/* endpoints to read the dashcam's current
settings and safely change them over the Novatek netapp HTTP interface
(http://<cam>/?custom=1&cmd=<id>&par=<value>).

- viofosync_lib/_control.py: settings read (cmd=3014) decoded against a
  derived per-model command map; validated writes with read-back verify; a
  hard denylist of destructive commands (format / factory reset / firmware
  update / delete / reboot / restart-wifi / SSD format+delete) refused before
  any request is built; auto stop/resume recording for record-gated settings;
  gentle single-request transport so the camera's single-threaded daemon is
  never overrun.
- web/routers/control.py + web/app.py: session/CSRF-protected REST API.
- web/static: Camera tab UI — toggles for on/off settings, drop-downs for the
  rest, a recording pill, and read-only rows (with reasons) for settings the
  camera can't change now (resolution over Wi-Fi, exposure, lenses not
  attached). Lens-dependent settings re-enable automatically when the lens is
  connected (detected via the live sensor count).
- viofosync_lib/data/command_map.json + scripts/build_command_map.py: a
  derived command/option map. Only the factual API data (command ids, keys,
  option enumerations) is reformatted from the VIOFO app's device-cmd-manager
  .db asset; the .db itself is not redistributed.
- tests/test_control.py: offline unit tests (denylist, validation, support
  classification, read-back verify, record-gated retry).

Validated end-to-end against an A329S; other models are mapped from the app
data but untested.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
droomurray added a commit to droomurray/viofosync that referenced this pull request Jun 16, 2026
Fork-only README banner explaining this fork adds the Camera control
feature and directing users to RobXYZ/viofosync for the upstream project
and standard functionality. Not part of PR RobXYZ#21.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Shows the Camera control UI on an A329S — toggles, the recording pill,
and the read-only rows (resolution/exposure) the camera won't change
over Wi-Fi.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@droomurray droomurray mentioned this pull request Jun 16, 2026
Satisfies the repo's `ruff check` CI step (I001).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@droomurray

Copy link
Copy Markdown
Contributor Author

This PR adds a Camera tab and /api/camera/* endpoints to read and adjust the dashcam's own settings over Wi-Fi (parking mode, watermarks, HDR, LEDs, GPS, loop length, bitrate, etc.), alongside the existing sync/browse/export features.

How it was built — I wanted the mapping to be grounded rather than guessed, so it came together in three stages:

  1. Firmware analysis. I statically analysed the A329S firmware (the cardv binary on the Novatek NA51102) to confirm the undocumented netapp HTTP grammar — http://<cam>/?custom=1&cmd=<id>&par=<value> with XML replies, the cmd=3014 settings dump, and the rough command-id ranges. That gave the protocol shape but not what each id means.
  2. App decompilation. To get real semantics I decompiled the official VIOFO Android app (apkeep → jadx) and found it ships a SQLite asset (device-cmd-manager.db) mapping every command id to a key, description, and option enumeration, per camera model. I reformatted just that factual API data into command_map.json — the app's .db itself is NOT redistributed, and scripts/build_command_map.py regenerates it.
  3. Automated testing against a real camera. I wrote a harness that drives the actual web API and round-trips every setting against my A329S — read current value, write a different valid value, confirm it applied by re-reading the camera's live config, then restore the original — exporting the full settings dump before and after to prove nothing was left changed. That empirical pass is where the safety behaviour came from: it's how I found that some "reads" are actually destructive actions (a bare cmd=3010 is format SD, and 3013 wedges the daemon), which settings are silently rejected while recording (so the layer briefly stops/restarts recording for those), and which simply can't be set over Wi-Fi (resolution) or need a lens that isn't attached.

Safeguards that came out of it:

  • Destructive commands (format, factory reset, firmware update, delete, reboot, restart-Wi-Fi, SSD format/delete) are on a hard denylist — refused before any request is built and never shown in the UI. Writes are allow-listed to enumerated settings, validated against the camera's option list, and read back to verify. One request at a time so it won't overrun the camera's single-threaded daemon.
  • Honest about limits. Settings the camera won't change over Wi-Fi (resolution, exposure) or that need an unattached lens are shown read-only with the reason; the lens-dependent ones re-enable automatically when the rear/interior camera is connected.
  • Tests + docs. Offline unit tests for the denylist/validation/verify logic, plus a README section.

One thing I'd flag for your call: the derived command map is reformatted from VIOFO's app data — if you'd rather handle it differently, I'm happy to change it. And it's only been validated against my A329S; other models are mapped but untested.

Heads up on CI: the initial run tripped on a ruff import-order nit in the test file — I've fixed that (ruff check is clean and the tests pass), so it just needs your approval to re-run the workflow.

No pressure at all to merge — your project, your call. Happy to adjust anything (squash, trim the data file, rework the UI to match your conventions, whatever helps).

@RobXYZ RobXYZ mentioned this pull request Jun 17, 2026
Merged
RobXYZ added a commit that referenced this pull request Jun 18, 2026
### Added

#### Camera Control

A new **Camera** tab reads the dashcam's current settings and lets you adjust the safely-changeable ones over Wi-Fi — on/off toggles, drop-downs, and a recording indicator — each validated against the camera's own option list and read back to confirm it stuck. Destructive commands (format SD, factory reset, firmware update, delete, reboot) are hard-blocked and never shown, and the few record-only settings auto-pause then resume recording. Settings for 29 Viofo models are mapped from the official app's command database; the A329S is validated on hardware. Contributed by [@droomurray](https://github.com/droomurray) (#21).

#### Three-Camera Support (Telephoto / Interior)

Telephoto (`T`) and interior/cabin (`I`) clips are now first-class alongside front and rear. They sync, index, and pair into the same capture group, so a three-camera day shows a third thumbnail in the archive and a third track on the timeline. New exports cover them: **Join Tele** / **Join Interior**, plus picture-in-picture with the third camera fullscreen and the front camera as the inset (the front clip stays the audio source, so the microphone track is preserved). The clip viewer's camera key cycles through every camera present at a timestamp. Two-camera setups are visually unchanged. Contributed by [@jusii](https://github.com/jusii) (#18).

#### Background Thumbnails & Filmstrips

Thumbnails and timeline filmstrips are now produced by a background worker as clips download — and existing clips are backfilled — so the archive and timeline populate as soon as footage arrives instead of after a sync cycle finishes. A new **Thumbnails** settings section controls it: thumbnail pre-generation is on by default, while the heavier filmstrip pre-generation is opt-in and otherwise falls back to generating on demand the first time a clip is viewed.

#### Per-Segment Picture-in-Picture in the Editor

The timeline editor's switched-camera cut can now carry a picture-in-picture inset on a per-segment basis. With a segment selected, press the PiP button (or **P**) to cycle the inset through your other cameras — it skips the segment's own camera — and a green placeholder shows where it will sit. The choice is remembered per segment and composited into the export, in the corner set by the global picture-in-picture position setting. A segment whose chosen camera has no overlapping footage simply exports without the inset.

#### Skip Downloads

You can now skip clips you don't want to sync. Select them in the download queue and choose **Skip** from the **Actions** menu; skipped clips get their own badge and are never downloaded. **Clear skip** returns them to the queue with a fresh set of retry attempts. Queue selection now spans pending, failed, and skipped clips, so one Actions menu — Download next / Skip / Clear skip / Retry failed — drives the whole list.

### Changed

- On/off controls across the app are now toggle switches, with one tooltip style used app-wide that works on hover, keyboard focus, and tap.
- The sync **pause** state is remembered across restarts instead of resetting to running.
- The download queue's per-action buttons are now a single **Actions** menu with **Apply**; selection-based **Retry failed** replaces the old retry-all button, while **Download recent hours next** is unchanged.
- Archive thumbnails animate on hover, scrubbing the clip's filmstrip — the same preview the Export Jobs list already offered. They fall back to the static thumbnail when no filmstrip is available, and respect reduced-motion.

### Fixed

- Locking a clip on the dashcam between sync cycles moves it into the camera's `/RO` folder; the download queue now refreshes the clip's source path when the camera re-reports it there, instead of exhausting its retry budget against the stale path and never syncing the clip. The dashcam-delete lock guard benefits too, since it keys off the same refreshed `/RO` path. Contributed by [@jusii](https://github.com/jusii) (#17).
- The Logs view now shows the date alongside the time and stays readable on a phone — long lines wrap rather than being clipped off-screen.
- Tapping a drop-down or text field no longer zooms the page in on mobile, and the interface no longer pinch-zooms — it behaves like a fixed app viewport.
- Auto-detected journeys on the timeline no longer miss the start of a drive or cut short on arrival — the journey window is padded past the GPS stop radius to take in the pull-away and pull-in clips, bounded by the surrounding parking footage.
@RobXYZ RobXYZ merged commit 32e63dd into RobXYZ:main Jun 18, 2026
1 check passed
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.

2 participants