Skip to content

The clock bridge gets a reader, and states what its bound is worth - #287

Open
Quchaosheng wants to merge 1 commit into
pollen-robotics:mainfrom
Quchaosheng:clock-consumer
Open

Quchaosheng wants to merge 1 commit into
pollen-robotics:mainfrom
Quchaosheng:clock-consumer

Conversation

@Quchaosheng

Copy link
Copy Markdown

Closes nothing on its own — this is the work #220 asks for, and the check it asks to have made first.

What was missing

robot.state, tof.frame and head_imu.frame carry t_ns (CLOCK_MONOTONIC), and media.video publishes the mono_ns/real_ns pair read together — that is the boot epoch the issue asks for, and it landed at v24. What did not exist was anything on the receiving side: the pair had no reader anywhere in the repo.

What this does

  • proto::clock::ClockPair — the bridge as a type. media.video's params now read their pair through ClockPair::now() rather than as two bare adjacent calls, which puts "these two readings belong together" in the type instead of in a comment and leaves no way for the next caller to read them a second apart. ClockPair::wall() is the receiving end: the addition that turns a t_ns into a wall-clock moment. It has no caller in this workspace, and the doc comment says so — the receivers that need it are the ones §11 describes, a peer that reads RTP rather than a robot daemon.
  • A clock row in the console — telemetry now shows a robot.state sample's wall-clock moment and its age.
  • §11 rewritten. It said wanted / not built, which stopped being true at v24 and was never corrected. AGENTS.md says the doc is the bug when it disagrees with behaviour.

The check §11 asked for first, and what it found

abs-capture-time is a dead end: webrtcsink configures TWCC and color-space and nothing else (0.15.3 and main), aiortc does not parse it, and neither the WebRTC specification nor the stats one has a capture-time member.

So frames are dated from RTCP sender reports, whose NTP time is the realtime clock by default (rtpmanager's ntp-time-source is ntp). The part the check turned up that the section did not expect: this only works for one kind of receiver. Dating a frame needs the report's RTP time as well as its NTP time, and only GStreamer exposes both (sr-rtptime beside sr-ntptime in rtpsession's stats). aiortc and every browser see the NTP half alone, so they can say what the sender's clock read and not which frame it was.

The bound

The issue asks for something written down rather than inherited from the network. The kernel's own ceiling on NTP's frequency discipline is MAXFREQ, 500 ppm of the pair's age — 30 µs over a minute, 1.8 ms over an hour.

That covers slewing, not a step, and the step is not hypothetical here: this board has no battery-backed RTC, boots at 1970, and NTP steps the clock (scripts/provision.sh says so, and pad-link-test.sh refuses the wall clock for the same reason). A pair taken before that first step is wrong by decades while looking like an ordinary number. Whether the clock has been set is updater's judgement — updater/src/preflight.rs owns that floor — so this points there rather than adding a second threshold to keep in step.

Verification

cargo test -p duck-ipc-proto — 53 passed. cargo fmt --check clean, clippy -D warnings clean. Additive throughout, so no API_VERSION bump.

One caveat: cargo test -p mediad needs the GStreamer C libraries, which the machine this was written on could not install, so the session.rs and console changes have not been through mediad's tests. The session.rs change is mechanical (two calls → one struct field) and ClockPair itself is covered.

Assisted-by: DeepSeek v4.1 via DSH

robot.state, tof.frame and head_imu.frame already carry t_ns, and media.video
already publishes the mono_ns/real_ns pair (v24). Nothing consumed the pair: the
bridge was on the wire with no reader.

proto::clock::ClockPair is that bridge as a type. media.video's params now read
their pair through ClockPair::now rather than as two bare adjacent calls, which
puts "these two readings belong together" in the type instead of in a comment
above them and leaves no way for the next caller to read them a second apart.
ClockPair::wall is the receiving end of the same contract — the addition that
turns a t_ns into a wall-clock moment. It has no caller in this workspace, and
that is stated rather than glossed: the receivers that need it are the ones §11
describes, a peer that reads RTP rather than a robot daemon.

The offset is signed, because a sample can predate the pair it is converted
with, and the result is clamped rather than allowed to wrap.

The doc comment is where the bound §11 asked for lives, and getting it right
took two attempts. The first claimed Linux slews rather than steps NTP
adjustments, which is false of CLOCK_REALTIME and false of this hardware in
particular: the board has no battery-backed RTC, boots at 1970, and NTP steps the
clock during provisioning (scripts/provision.sh says so; pad-link-test.sh
refuses the wall clock for the same reason). So the bound is stated as two parts
— 500 ppm of the pair's age, which is the kernel's own MAXFREQ on NTP's frequency
discipline, and the step case that no arithmetic over the pair can see.

That second part is deliberately not a method. Whether the robot's clock has been
set is updater's judgement, and updater/src/preflight.rs already makes it, with a
floor and the same reasoning. A copy here would be a second threshold to keep in
step with the first, and it would sit in the one crate meant to stay nearly
dependency-free because btd is part of the recovery path.

The console's telemetry grows a clock row. It makes the same addition the browser
cannot ask Rust for, and refuses to print an age that is obviously a missing sync
rather than a slow network. It stays on the methods the page already uses rather
than reaching for padStart or toTimeString, which exist nowhere in this
repository.

§11 said "wanted" and "not built", which stopped being true at v24 and was never
corrected. It now says what was built, and what the check the section asked for
found. abs-capture-time is a dead end — webrtcsink (0.15.3 and main), aiortc, and
neither the WebRTC specification nor the stats one surface it — so frames are
dated from RTCP sender reports, whose NTP time is the realtime clock by default.
What the check turned up that the section did not expect is that this works for
exactly one kind of receiver: dating a frame needs the report's RTP time as well
as its NTP time, and only GStreamer exposes both (sr-rtptime alongside
sr-ntptime). aiortc and every browser see the NTP half alone.

Additive, so no API_VERSION bump.

Co-authored-by: cyathea152 <cyathea152@gmail.com>
Assisted-by: DeepSeek v4.1 via DSH
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