Skip to content

ONVIF camera control + cargo-dist release pipeline#10

Merged
xinquiry merged 4 commits into
mainfrom
feat/onvif-camera-control
Jun 15, 2026
Merged

ONVIF camera control + cargo-dist release pipeline#10
xinquiry merged 4 commits into
mainfrom
feat/onvif-camera-control

Conversation

@xinquiry

@xinquiry xinquiry commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two threads, separate concerns, but small enough to land together:

Camera control plane (d622ef5)

ONVIF cameras with a control: block in media_sources now register as Devices alongside their existing streaming path, so osdl send cam1 ptz_move|ptz_stop|ptz_preset_goto|snapshot flows through the standard send_command pipeline. No new RPCs, no new CLI subcommand. Streaming behavior is unchanged for cameras without a control: block — fully backward compatible.

  • adapter/onvif.rs — built-in adapter (no YAML registry); JSON-envelope encoding so the byte-oriented Transport trait stays uniform; direction shorthand (up/down/left/right/zoom_in/zoom_out) plus explicit pan/tilt/zoom vectors.
  • transport/onvif.rs — HTTP/SOAP client with WS-Security UsernameToken digest auth; quick-xml parsing of GetCapabilities/GetProfiles/GetSnapshotUri (replaces an earlier hand-rolled XML walker that was vulnerable to lookalike tags); per-camera auto-stop tracker so back-to-back ptz_move calls or an explicit ptz_stop cancel a prior in-flight stop task.
  • engine.rs — walks media_sources after mediamtx spawns and dual-registers cameras with control: set. Idempotent across mediamtx restarts.
  • config.rs — adds OsdlConfig.data_dir so snapshots have a stable home (<data_dir>/snapshots/<cam_id>/<ts>.jpg); path/URL surface as device_status properties.
  • Verified live against two JZT31 cameras: PTZ in all directions, auto-stop cancellation across overlapping moves, and a 372 KB snapshot saved + reopened cleanly.

Release pipeline via cargo-dist (5c2a199 + fba14a7)

dist init configures GitHub Actions to build pre-built binaries for every tagged v* push. Daily development is unaffected — PRs trigger a 30s validation-only dist plan run; releases only fire on tag push.

  • Targets: Apple Silicon + Intel macOS, glibc + musl Linux on x86_64 + aarch64, x86_64 Windows.
  • glibc floor: 2.17 on x86_64 (RHEL 7+, Ubuntu 14.04+), 2.28 on aarch64 (no real aarch64 distro shipped older). Same target uv pins. Without this, cargo-dist would silently link against ubuntu-22.04's GLIBC_2.31 and shut RHEL 7 / CentOS 7 users out.
  • Installers: installer.sh (curl-pipe-sh) and installer.ps1 (irm-iex). Auto-detect OS/arch/libc and drop into \$CARGO_HOME/bin. README updated with one-liner install commands.
  • Updater: osdl-update companion binary so users can self-upgrade without checking releases manually.
  • Propagates repository = … to every workspace member via workspace.package so the GitHub-CI integration knows where to publish.

Housekeeping (978738d)

Remove firmware_status.md — was a one-off bring-up snapshot superseded by per-MCU firmware crate layout (#7). Live cargo build matrix is the source of truth now.

Test plan

  • cargo test --workspace --features espnow — 161 tests pass (unit + integration + e2e)
  • Live PTZ + snapshot regression on cam131/cam53 (192.168.1.131 / .53), including auto-stop cancellation across overlapping ptz_move calls
  • cargo zigbuild --target x86_64-unknown-linux-gnu.2.17 --features espnow --bin osdl — confirms glibc 2.17 binary builds with the new ONVIF deps (verified via objdump symbol-version walk)
  • dist plan succeeds with the configured targets + glibc pin
  • First tag push (v0.1.0) — deferred until this lands; will validate the actual workflow run

Out of scope

  • Snapshot HTTP serving — snapshot_url is currently file:// only. Mediamtx already runs an HTTP listener on :8888; teaching it to serve <data_dir>/snapshots/ would let us return a real URL for cloud-WS consumers. Tracked in the Xyzen-side integration doc (Xyzen/osdl-camera-control.md).
  • HTTP Digest auth — only WS-Security UsernameToken is implemented. Cameras requiring HTTP Digest will need a follow-up.
  • ONVIF Media v2 (tr2:Media2) — Media v1 only for now.
  • Homebrew tap publishing — left out of installers until someone asks; it's a one-line config flip.

🤖 Generated with Claude Code

Sourcery 总结

在引擎中将 ONVIF 摄像机控制作为一等设备支持,并集成基于 cargo-dist 的发布管线,包括安装脚本和元数据更新。

新功能:

  • 通过新的适配器和传输层暴露 ONVIF 摄像机的 PTZ 和快照控制,并根据 media_sources 配置将可控摄像机注册为设备。
  • 支持按摄像机分别生成快照,写入可配置的 data_dir,并通过设备状态事件暴露快照路径。
  • 使用 cargo-dist 为主流平台提供预构建二进制和安装程序,包括用于自升级的 osdl-update 辅助工具。

增强:

  • 将 CLI 解析出的数据目录传播到引擎配置中,使运行时组件共享稳定的存储位置。
  • 在 README 和示例配置配方中记录 ONVIF 摄像机控制的使用方式以及二进制安装方法。
  • 为工作区 crate 添加仓库元数据,并配置专用的 dist 配置档用于优化发布构建。

构建:

  • 添加 cargo-dist 工作区配置及目标矩阵和 glibc 最低版本限制,同时引入 ONVIF 支持所需的新 HTTP/XML 依赖,以及专用的 dist 构建配置档。

CI:

  • 引入由 cargo-dist 驱动的 GitHub Actions 工作流,在打版本标签时构建、打包并发布发布制品,并在拉取请求上运行轻量级计划检查。

文档:

  • 为所有支持的平台记录一行安装命令,并在示例摄像机配置中描述 ONVIF 控制相关配置。

测试:

  • 为 ONVIF 适配器的编码/解码、传输层 XML 解析辅助函数以及时间戳格式化工具添加单元测试,以验证控制和快照行为。

杂务:

  • 移除已过时的 firmware_status.md 快照,改用当前的 firmware crate 布局。
Original summary in English

Summary by Sourcery

Add ONVIF camera control as a first-class device in the engine and integrate a cargo-dist based release pipeline with install scripts and metadata updates.

New Features:

  • Expose ONVIF camera PTZ and snapshot control via a new adapter and transport, registering controllable cameras as devices driven from media_sources configuration.
  • Support per-camera snapshots written under a configurable data_dir, with paths surfaced via device status events.
  • Provide pre-built binaries and installers for major platforms using cargo-dist, including an osdl-update helper for self-upgrade.

Enhancements:

  • Propagate CLI-resolved data directory into engine configuration so runtime components share a stable storage location.
  • Document ONVIF camera control usage and binary installation in README and example config recipes.
  • Add repository metadata to workspace crates and configure a dedicated dist profile for optimized release builds.

Build:

  • Add cargo-dist workspace configuration with target matrix and glibc minimums, along with new HTTP/XML dependencies needed for ONVIF support and a dedicated dist build profile.

CI:

  • Introduce a cargo-dist powered GitHub Actions workflow to build, package, and publish release artifacts on version tags, while running lightweight plan checks on pull requests.

Documentation:

  • Document one-line install commands for all supported platforms and describe ONVIF control configuration in example camera configs.

Tests:

  • Add unit tests around ONVIF adapter encoding/decoding, transport XML parsing helpers, and timestamp formatting utilities to validate control and snapshot behavior.

Chores:

  • Remove the obsolete firmware_status.md snapshot in favor of the current firmware crate layout.

xinquiry and others added 4 commits June 15, 2026 17:32
Cameras with `media_sources[].control:` now register as Devices alongside
their streaming path, so `osdl send cam1 ptz_move|ptz_stop|ptz_preset_goto|
snapshot` flows through the existing send_command pipeline. No new RPCs,
no camera-specific subcommand.

Streaming behavior is unchanged for cameras without a `control` block.

- adapter/onvif.rs: built-in adapter (no YAML registry); JSON-envelope
  encoding so the byte-oriented Transport trait stays uniform; direction
  shorthand (up/down/left/right/zoom_in/zoom_out) plus explicit pan/tilt/
  zoom vectors.
- transport/onvif.rs: HTTP/SOAP client with WS-Security UsernameToken
  digest auth; quick-xml parsing of GetCapabilities/GetProfiles/
  GetSnapshotUri; per-camera auto-stop tracker so back-to-back ptz_move
  calls or an explicit ptz_stop cancel a prior in-flight stop task.
- engine: walks media_sources after mediamtx spawns and dual-registers
  cameras with `control:` set. Idempotent across mediamtx restarts.
- config: OsdlConfig.data_dir threaded through; snapshots land under
  <data_dir>/snapshots/<cam_id>/<ts>.jpg with path/url surfaced as
  device_status properties.

Verified live against two JZT31 cameras: PTZ in all directions, auto-stop
cancellation across overlapping moves, and a 372 KB snapshot saved to
disk and reopened cleanly.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
`dist init` configures GitHub Actions to build pre-built binaries for
every tagged `v*` push. Targets cover Apple Silicon + Intel macOS,
glibc and musl Linux on x86_64 + aarch64, and x86_64 Windows. Floor
glibc 2.17 (RHEL 7+, Ubuntu 14.04+) via cargo-zigbuild + the
`min-glibc-version` knob. Shell + PowerShell installers are generated
so users get the standard `curl … | sh` / `irm … | iex` install path.

Also propagates `repository = …` to every workspace member via
`workspace.package` so cargo-dist's GitHub-CI integration knows where
to publish.

README gains an Install section pointing at the installer scripts.
Daily development is unaffected — the workflow only publishes when a
SemVer tag is pushed; PRs trigger a validation-only `dist plan` run.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
The file was a one-off snapshot of in-flight firmware work that was
superseded by the per-MCU layout in firmware/ (commit e2304a1) and
the per-station notes living next to each binary's source. Keeping
it around just rots — the live `cargo build` matrix is the source of
truth now.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
cargo-dist defaults to the runner's glibc (2.31+ on ubuntu-22.04),
which silently shuts RHEL 7 / CentOS 7 / Ubuntu 14.04-18.04 out of
running our binaries. 2.17 is the lowest practical floor — same
target Astral's uv pins, same one we've already verified locally
with `cargo zigbuild --target=...gnu.2.17`. aarch64 needs 2.28
because no real aarch64 distro ever shipped older.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
@sourcery-ai

sourcery-ai Bot commented Jun 15, 2026

Copy link
Copy Markdown

Reviewer's Guide

将 ONVIF 摄像机控制作为一等公民的设备/传输方式(PTZ 和快照),并将其接入现有的 send_command 流水线;引入可配置的 data_dir 用于快照存储;更新 CLI 和示例配置以使用新的路径和适配器;同时基于 cargo-dist 配置 GitHub Actions 发布流水线,提供预构建二进制和安装器,并清理过时的固件文档和工作区元数据。

通过 send_command 进行 ONVIF 摄像机控制的时序图

sequenceDiagram
    actor User
    participant OsdlCli as osdl_cli
    participant Engine as OsdlEngine
    participant Adapter as OnvifAdapter
    participant Transport as OnvifTransport
    participant Cam as OnvifCamera

    User->>OsdlCli: osdl send cam1 ptz_move -p direction=up
    OsdlCli->>Engine: send_command(device_id=cam1, action=ptz_move)
    Engine->>Adapter: encode_command(device_type=onvif_camera_ptz, cmd)
    Adapter-->>Engine: JSON envelope { op: "ptz_move", args: ... }
    Engine->>Transport: send(bytes=envelope)
    Transport->>Transport: dispatch("ptz_move", args)
    Transport->>Cam: SOAP ContinuousMove + scheduled Stop
    Transport-->>Engine: TransportRx{ transport_id, data=JSON result }
    Engine->>Adapter: decode_response(device_type, bytes)
    Adapter-->>Engine: properties (e.g. ptz_move_ok)
    Engine-->>OsdlCli: DeviceStatus event

    User->>OsdlCli: osdl send cam1 snapshot
    OsdlCli->>Engine: send_command(device_id=cam1, action=snapshot)
    Engine->>Adapter: encode_command(... snapshot ...)
    Adapter-->>Engine: JSON envelope { op: "snapshot" }
    Engine->>Transport: send(bytes=envelope)
    Transport->>Cam: SOAP GetSnapshotUri + HTTP GET JPEG
    Transport->>Transport: write JPEG to <data_dir>/snapshots/cam1/<ts>.jpg
    Transport-->>Engine: TransportRx{ data: { op:"snapshot", ok:true, data:{ path, url } } }
    Engine->>Adapter: decode_response(...)
    Adapter-->>Engine: properties (snapshot_path, snapshot_url)
    Engine-->>OsdlCli: DeviceStatus with snapshot info
Loading

File-Level Changes

Change Details Files
引入 ONVIF HTTP/SOAP 传输层和适配器,并将带控制块的 ONVIF 摄像机注册为 Devices,使 PTZ/快照命令通过标准的 send_command 路径流转。
  • 实现 OnvifTransport,作为一个接受 JSON 封装数据的 Transport 实现,执行 PTZ move/stop/preset 和 Media snapshot 的 WS-Security UsernameToken SOAP 调用,管理每台摄像机的端点发现,将快照保存到磁盘,并处理 PTZ 自动停止的取消。
  • 实现 OnvifAdapter,作为一个 ProtocolAdapter 暴露 PTZ 和快照动作,验证/规范化命令参数(方向简写 vs 显式向量),将其编码为 JSON 封装数据,并将 JSON 响应解码为扁平化的设备属性。
  • 导出 onvif 传输模块和辅助工具,例如稳定的 onvif:{camera_id} 传输 ID,以及覆盖 XML 解析、WS-Security 头结构和时间转换工具的单元测试。
crates/osdl-core/src/transport/onvif.rs
crates/osdl-core/src/adapter/onvif.rs
crates/osdl-core/src/transport/mod.rs
将 ONVIF 控制接入引擎和 CLI,使带控制块的摄像机成为 Devices,并让快照使用稳定的数据目录。
  • 扩展 OsdlConfig,增加可选的 data_dir,并让 CLI 的 serve 命令在启动引擎前解析并注入最终生效的 data_dir(CLI 参数或平台默认)到引擎配置中。
  • 增加引擎逻辑以遍历 media_sources,检测带控制块的 OnvifCamera 条目,为每台摄像机分配一个 OnvifTransport,注册一个 ONVIF 组合摄像机/PTZ Device,附带相应动作,并在确保跨重启幂等的前提下广播更新后的状态。
  • 定义 OnvifCameraConfig.control 和配套的 OnvifControlConfig 字段,用于保存 ONVIF 端点/认证/媒体配置文件等信息,同时提供一个共享的 SNAPSHOT_SUBDIR 常量用于快照存放路径,并更新测试以适配新字段。
crates/osdl-core/src/engine.rs
crates/osdl-core/src/config.rs
crates/osdl-core/src/media/onvif_camera.rs
crates/osdl-cli/src/commands/serve.rs
更新文档和示例配置,以描述和演示 ONVIF 控制及快照行为。
  • 扩展 ONVIF 摄像机 recipe,包含可选的 control 部分,文档化 PTZ 和快照的 send_command 用法,并解释快照文件布局以及暴露的 device_status 属性。
  • 补充 JZT31 摄像机 recipe,加入真实的控制块(onvif_url、用户名/密码)用于两台匹配测试环境的摄像机,以演示典型配置。
  • README 中文档化基于安装器的使用方式和二进制安装,将安装说明与从源码构建分离,并提及 osdl-update
docs/recipes/configs/onvif-camera.yaml
docs/recipes/configs/cameras-jzt31.yaml
README.md
使用 cargo-dist 配置基于 GitHub Actions CI 的发布工具链,提供预构建二进制、安装器,并调整工作区元数据。
  • 新增 dist-workspace.toml,配置 cargo-dist 版本、支持的目标平台、安装器(shell/PowerShell)、安装位置、更新器二进制,以及每个目标的最小 glibc 版本,以保持对旧版 Linux 发行版的支持。
  • 引入 GitHub Actions Release 工作流,在 PR 上运行 dist plan,在 tag push 时为一组目标矩阵构建制品,将其上传为 artifacts,并发布到 GitHub Releases,同时生成发布说明。
  • 设置工作区级别的仓库元数据并传播到各 crate,在 workspace.dependencies 下添加新的 HTTP/XML 依赖(reqwestsha1base64quick-xml),并定义一个针对 cargo-dist 构建优化的 [profile.dist]
  • 移除过时的 firmware_status.md,以当前的 firmware crate 布局和在线构建矩阵作为唯一可信来源。
dist-workspace.toml
.github/workflows/release.yml
Cargo.toml
crates/osdl-core/Cargo.toml
crates/osdl-cli/Cargo.toml
crates/osdl-firmware-protocol/Cargo.toml
crates/osdl-proto/Cargo.toml
crates/osdl-server/Cargo.toml
firmware_status.md
Cargo.lock

Tips and commands

Interacting with Sourcery

  • 触发新的审查: 在 pull request 中评论 @sourcery-ai review
  • 继续讨论: 直接回复 Sourcery 的审查评论。
  • 从审查评论生成 GitHub issue: 在某条审查评论下回复,请求 Sourcery 从该评论创建一个 issue。你也可以直接回复该评论 @sourcery-ai issue 来从中创建 issue。
  • 生成 pull request 标题: 在 pull request 标题中的任意位置写上 @sourcery-ai,即可随时生成标题。你也可以在 pull request 中评论 @sourcery-ai title 来(重新)生成标题。
  • 生成 pull request 摘要: 在 pull request 正文的任意位置写上 @sourcery-ai summary,即可在指定位置生成 PR 摘要。你也可以在 pull request 中评论 @sourcery-ai summary 来(重新)生成摘要。
  • 生成审查者指南: 在 pull request 中评论 @sourcery-ai guide,即可在任意时间(重新)生成审查者指南。
  • 一次性解决所有 Sourcery 评论: 在 pull request 中评论 @sourcery-ai resolve,以标记所有 Sourcery 评论为已解决。如果你已经处理完所有评论且不希望再看到它们,这会非常有用。
  • 忽略所有 Sourcery 审查: 在 pull request 中评论 @sourcery-ai dismiss,以忽略所有现有 Sourcery 审查。特别适用于你希望从一次全新的审查开始 —— 记得再评论 @sourcery-ai review 触发新的审查!

Customizing Your Experience

访问你的 dashboard 以:

  • 启用或禁用审查功能,例如 Sourcery 生成的 pull request 摘要、审查者指南等。
  • 更改审查语言。
  • 添加、移除或编辑自定义审查说明。
  • 调整其他审查设置。

Getting Help

Original review guide in English

Reviewer's Guide

Adds ONVIF camera control as a first-class device/transport (PTZ and snapshots) and wires it into the existing send_command pipeline, introduces a configurable data_dir for snapshot storage, updates the CLI and example configs to use the new path and adapter, and sets up a cargo-dist based GitHub Actions release pipeline with prebuilt binaries and installers while cleaning up obsolete firmware docs and workspace metadata.

Sequence diagram for ONVIF camera control via send_command

sequenceDiagram
    actor User
    participant OsdlCli as osdl_cli
    participant Engine as OsdlEngine
    participant Adapter as OnvifAdapter
    participant Transport as OnvifTransport
    participant Cam as OnvifCamera

    User->>OsdlCli: osdl send cam1 ptz_move -p direction=up
    OsdlCli->>Engine: send_command(device_id=cam1, action=ptz_move)
    Engine->>Adapter: encode_command(device_type=onvif_camera_ptz, cmd)
    Adapter-->>Engine: JSON envelope { op: "ptz_move", args: ... }
    Engine->>Transport: send(bytes=envelope)
    Transport->>Transport: dispatch("ptz_move", args)
    Transport->>Cam: SOAP ContinuousMove + scheduled Stop
    Transport-->>Engine: TransportRx{ transport_id, data=JSON result }
    Engine->>Adapter: decode_response(device_type, bytes)
    Adapter-->>Engine: properties (e.g. ptz_move_ok)
    Engine-->>OsdlCli: DeviceStatus event

    User->>OsdlCli: osdl send cam1 snapshot
    OsdlCli->>Engine: send_command(device_id=cam1, action=snapshot)
    Engine->>Adapter: encode_command(... snapshot ...)
    Adapter-->>Engine: JSON envelope { op: "snapshot" }
    Engine->>Transport: send(bytes=envelope)
    Transport->>Cam: SOAP GetSnapshotUri + HTTP GET JPEG
    Transport->>Transport: write JPEG to <data_dir>/snapshots/cam1/<ts>.jpg
    Transport-->>Engine: TransportRx{ data: { op:"snapshot", ok:true, data:{ path, url } } }
    Engine->>Adapter: decode_response(...)
    Adapter-->>Engine: properties (snapshot_path, snapshot_url)
    Engine-->>OsdlCli: DeviceStatus with snapshot info
Loading

File-Level Changes

Change Details Files
Introduce an ONVIF HTTP/SOAP transport and adapter, and register ONVIF cameras with control blocks as Devices so PTZ/snapshot commands flow through the standard send_command path.
  • Implement OnvifTransport as a Transport impl that accepts JSON envelopes, performs WS-Security UsernameToken SOAP calls for PTZ move/stop/preset and Media snapshot, manages per-camera endpoint discovery, saves snapshots to disk, and handles PTZ auto-stop cancellation.
  • Implement OnvifAdapter as a ProtocolAdapter that exposes PTZ and snapshot actions, validates/normalizes command parameters (direction shorthand vs explicit vectors), encodes them into JSON envelopes, and decodes JSON responses into flattened device properties.
  • Export the onvif transport module and helper such as a stable onvif:{camera_id} transport id, plus unit tests covering XML parsing, WS-Security header structure, and time conversion utilities.
crates/osdl-core/src/transport/onvif.rs
crates/osdl-core/src/adapter/onvif.rs
crates/osdl-core/src/transport/mod.rs
Wire ONVIF control into the engine and CLI so cameras with a control block become Devices and snapshots use a stable data directory.
  • Extend OsdlConfig with an optional data_dir, and have the CLI serve command resolve and inject the effective data_dir (CLI flag or platform default) into the engine config before startup.
  • Add engine logic to walk media_sources, detect OnvifCamera entries with a control block, allocate an OnvifTransport per camera, register an ONVIF combined camera/PTZ Device with appropriate actions, and broadcast updated status while ensuring idempotence across restarts.
  • Define OnvifCameraConfig.control and supporting OnvifControlConfig fields to hold ONVIF endpoint/auth/profile info, plus a shared SNAPSHOT_SUBDIR constant for snapshot placement, and update tests to account for the new field.
crates/osdl-core/src/engine.rs
crates/osdl-core/src/config.rs
crates/osdl-core/src/media/onvif_camera.rs
crates/osdl-cli/src/commands/serve.rs
Update documentation and example configs to describe and exercise ONVIF control and snapshot behavior.
  • Expand the ONVIF camera recipe to include the optional control section, document send_command usage for PTZ and snapshots, and explain snapshot file layout and surfaced device_status properties.
  • Augment the JZT31 camera recipe with real control blocks (onvif_url, username/password) for two cameras matching the test setup, demonstrating typical configuration.
  • Document installer-based usage and binary installation in the README, separating install instructions from building from source and mentioning osdl-update.
docs/recipes/configs/onvif-camera.yaml
docs/recipes/configs/cameras-jzt31.yaml
README.md
Configure cargo-dist based release tooling with GitHub Actions CI, prebuilt binaries, installers, and workspace metadata adjustments.
  • Add a dist-workspace.toml configuring cargo-dist version, supported targets, installers (shell/PowerShell), install location, updater binary, and minimum glibc versions per target to keep older Linux distros supported.
  • Introduce a GitHub Actions Release workflow that runs dist plan on PRs, builds artifacts for a matrix of targets on tag pushes, uploads them as artifacts, and publishes them to GitHub Releases with generated notes.
  • Set workspace-level repository metadata and propagate it to crates, add new HTTP/XML dependencies (reqwest, sha1, base64, quick-xml) under workspace.dependencies, and define a [profile.dist] optimized for cargo-dist builds.
  • Remove the obsolete firmware_status.md in favor of the current firmware crate layout and live build matrix as the source of truth.
dist-workspace.toml
.github/workflows/release.yml
Cargo.toml
crates/osdl-core/Cargo.toml
crates/osdl-cli/Cargo.toml
crates/osdl-firmware-protocol/Cargo.toml
crates/osdl-proto/Cargo.toml
crates/osdl-server/Cargo.toml
firmware_status.md
Cargo.lock

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - 我发现了 1 个问题,并留下了一些总体层面的反馈:

  • ONVIF 传输层在异步方法(例如 do_snapshot)中执行了同步的文件系统操作(create_dir_allwrite);建议将这些操作切换到 tokio::fs 或使用 spawn_blocking,以避免在大文件或慢写入时阻塞异步运行时。
  • 用于 WS-Security 时间戳的自定义 epoch_to_ymdhms/format_created_now 日历计算逻辑比较复杂;使用经过充分测试的时间/日期库(例如 timechrono)可以降低出现细微 Bug 的风险,并简化实现。
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ONVIF transport does synchronous filesystem work (`create_dir_all`, `write`) inside async methods like `do_snapshot`; consider switching to `tokio::fs` or `spawn_blocking` for those operations to avoid blocking the async runtime on large/slow writes.
- The custom `epoch_to_ymdhms`/`format_created_now` calendar math for WS-Security timestamps is non-trivial; using a well‑tested time/date crate (e.g. `time` or `chrono`) would reduce the risk of subtle bugs and simplify the implementation.

## Individual Comments

### Comment 1
<location path="crates/osdl-core/src/transport/onvif.rs" line_range="331-340" />
<code_context>
+        } else {
+            req
+        };
+        let resp = req.send().await.map_err(|e| format!("snapshot GET: {e}"))?;
+        let status = resp.status();
+        let bytes = resp
+            .bytes()
+            .await
+            .map_err(|e| format!("snapshot read: {e}"))?;
+        if !status.is_success() {
+            return Err(format!("snapshot HTTP {status}"));
+        }
+
+        let now_ms = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .map(|d| d.as_millis())
+            .unwrap_or(0);
+        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
+        std::fs::create_dir_all(&cam_dir)
+            .map_err(|e| format!("create snapshot dir: {e}"))?;
+        let path = cam_dir.join(format!("{now_ms}.jpg"));
+        std::fs::write(&path, &bytes)
+            .map_err(|e| format!("write snapshot: {e}"))?;
+
</code_context>
<issue_to_address>
**suggestion (performance):** Blocking filesystem operations (`create_dir_all`/`write`) inside async code can stall the runtime under load.

These `std::fs::create_dir_all` and `std::fs::write` calls run directly in the async `do_snapshot` path and can block a Tokio worker thread under load, impacting other tasks. Prefer Tokio’s async filesystem APIs:

```rust
use tokio::fs;

fs::create_dir_all(&cam_dir)
    .await
    .map_err(|e| format!("create snapshot dir: {e}"))?;
fs::write(&path, &bytes)
    .await
    .map_err(|e| format!("write snapshot: {e}"))?;
```

If you must keep `std::fs`, consider wrapping these in `tokio::task::spawn_blocking` instead.

Suggested implementation:

```rust
use serde_json::{json, Value};
use sha1::{Digest, Sha1};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::fs;

```

```rust
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0);
        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
        fs::create_dir_all(&cam_dir)
            .await
            .map_err(|e| format!("create snapshot dir: {e}"))?;
        let path = cam_dir.join(format!("{now_ms}.jpg"));
        fs::write(&path, &bytes)
            .await
            .map_err(|e| format!("write snapshot: {e}"))?;

```
</issue_to_address>

Sourcery 对开源项目是免费的 —— 如果你觉得我们的评审有帮助,欢迎分享 ✨
帮我变得更有用!请在每条评论上点 👍 或 👎,我会根据你的反馈改进后续评审。
Original comment in English

Hey - I've found 1 issue, and left some high level feedback:

  • The ONVIF transport does synchronous filesystem work (create_dir_all, write) inside async methods like do_snapshot; consider switching to tokio::fs or spawn_blocking for those operations to avoid blocking the async runtime on large/slow writes.
  • The custom epoch_to_ymdhms/format_created_now calendar math for WS-Security timestamps is non-trivial; using a well‑tested time/date crate (e.g. time or chrono) would reduce the risk of subtle bugs and simplify the implementation.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The ONVIF transport does synchronous filesystem work (`create_dir_all`, `write`) inside async methods like `do_snapshot`; consider switching to `tokio::fs` or `spawn_blocking` for those operations to avoid blocking the async runtime on large/slow writes.
- The custom `epoch_to_ymdhms`/`format_created_now` calendar math for WS-Security timestamps is non-trivial; using a well‑tested time/date crate (e.g. `time` or `chrono`) would reduce the risk of subtle bugs and simplify the implementation.

## Individual Comments

### Comment 1
<location path="crates/osdl-core/src/transport/onvif.rs" line_range="331-340" />
<code_context>
+        } else {
+            req
+        };
+        let resp = req.send().await.map_err(|e| format!("snapshot GET: {e}"))?;
+        let status = resp.status();
+        let bytes = resp
+            .bytes()
+            .await
+            .map_err(|e| format!("snapshot read: {e}"))?;
+        if !status.is_success() {
+            return Err(format!("snapshot HTTP {status}"));
+        }
+
+        let now_ms = SystemTime::now()
+            .duration_since(UNIX_EPOCH)
+            .map(|d| d.as_millis())
+            .unwrap_or(0);
+        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
+        std::fs::create_dir_all(&cam_dir)
+            .map_err(|e| format!("create snapshot dir: {e}"))?;
+        let path = cam_dir.join(format!("{now_ms}.jpg"));
+        std::fs::write(&path, &bytes)
+            .map_err(|e| format!("write snapshot: {e}"))?;
+
</code_context>
<issue_to_address>
**suggestion (performance):** Blocking filesystem operations (`create_dir_all`/`write`) inside async code can stall the runtime under load.

These `std::fs::create_dir_all` and `std::fs::write` calls run directly in the async `do_snapshot` path and can block a Tokio worker thread under load, impacting other tasks. Prefer Tokio’s async filesystem APIs:

```rust
use tokio::fs;

fs::create_dir_all(&cam_dir)
    .await
    .map_err(|e| format!("create snapshot dir: {e}"))?;
fs::write(&path, &bytes)
    .await
    .map_err(|e| format!("write snapshot: {e}"))?;
```

If you must keep `std::fs`, consider wrapping these in `tokio::task::spawn_blocking` instead.

Suggested implementation:

```rust
use serde_json::{json, Value};
use sha1::{Digest, Sha1};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::fs;

```

```rust
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0);
        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
        fs::create_dir_all(&cam_dir)
            .await
            .map_err(|e| format!("create snapshot dir: {e}"))?;
        let path = cam_dir.join(format!("{now_ms}.jpg"));
        fs::write(&path, &bytes)
            .await
            .map_err(|e| format!("write snapshot: {e}"))?;

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +331 to +340
let resp = req.send().await.map_err(|e| format!("snapshot GET: {e}"))?;
let status = resp.status();
let bytes = resp
.bytes()
.await
.map_err(|e| format!("snapshot read: {e}"))?;
if !status.is_success() {
return Err(format!("snapshot HTTP {status}"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): 在异步代码中执行阻塞的文件系统操作(create_dir_all/write)会在高负载下拖慢运行时。

这些 std::fs::create_dir_allstd::fs::write 调用直接运行在异步的 do_snapshot 调用路径上,在高负载下可能阻塞 Tokio 的工作线程,影响其他任务。建议优先使用 Tokio 的异步文件系统 API:

use tokio::fs;

fs::create_dir_all(&cam_dir)
    .await
    .map_err(|e| format!("create snapshot dir: {e}"))?;
fs::write(&path, &bytes)
    .await
    .map_err(|e| format!("write snapshot: {e}"))?;

如果必须保留 std::fs,可以考虑把这些操作包在 tokio::task::spawn_blocking 中。

建议的实现:

use serde_json::{json, Value};
use sha1::{Digest, Sha1};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::fs;
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0);
        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
        fs::create_dir_all(&cam_dir)
            .await
            .map_err(|e| format!("create snapshot dir: {e}"))?;
        let path = cam_dir.join(format!("{now_ms}.jpg"));
        fs::write(&path, &bytes)
            .await
            .map_err(|e| format!("write snapshot: {e}"))?;
Original comment in English

suggestion (performance): Blocking filesystem operations (create_dir_all/write) inside async code can stall the runtime under load.

These std::fs::create_dir_all and std::fs::write calls run directly in the async do_snapshot path and can block a Tokio worker thread under load, impacting other tasks. Prefer Tokio’s async filesystem APIs:

use tokio::fs;

fs::create_dir_all(&cam_dir)
    .await
    .map_err(|e| format!("create snapshot dir: {e}"))?;
fs::write(&path, &bytes)
    .await
    .map_err(|e| format!("write snapshot: {e}"))?;

If you must keep std::fs, consider wrapping these in tokio::task::spawn_blocking instead.

Suggested implementation:

use serde_json::{json, Value};
use sha1::{Digest, Sha1};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tokio::fs;
        let now_ms = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .map(|d| d.as_millis())
            .unwrap_or(0);
        let cam_dir = self.inner.snapshot_root.join(&self.inner.camera_id);
        fs::create_dir_all(&cam_dir)
            .await
            .map_err(|e| format!("create snapshot dir: {e}"))?;
        let path = cam_dir.join(format!("{now_ms}.jpg"));
        fs::write(&path, &bytes)
            .await
            .map_err(|e| format!("write snapshot: {e}"))?;

@xinquiry xinquiry merged commit 8c3888f into main Jun 15, 2026
6 checks 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.

1 participant