Repository navigation
iPhone and watch: live numbers as they land through FTW Cloud - #72
Conversation
…land The box now sends a reading through FTW Cloud as its driver delivers it, up to five frames a second, with a beat each second when nothing moves. The ring repeats an unchanged value at most once a second and holds 1500 points. LiveLatencyTests times a reading from the box's driver to this session's decode against a local ftwcloudd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a75d1f38b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| var ring = rings[fid] ?? RingBuffer(capacity: Self.capacity) | ||
| let last = ring.last | ||
| if let last, atMs <= last.t { continue } | ||
| if let last, !last.restored, last.v == v, atMs - last.t < Self.repeatMs { continue } |
There was a problem hiding this comment.
Track suppressed frames before deciding gaps
When an unchanged field arrives at 5 Hz, this early return leaves its recorded timestamp up to 800 ms behind the most recent frame. If the carrier then pauses for 2.6–3.5 seconds—still within gapMs and potentially still considered live—the next point can appear more than 3.5 seconds after last.t, causing record and LiveSeries to create a false break in the trace. Keep a per-field last-observed timestamp (and current hold) even when suppressing the duplicate, and use that timestamp for gap detection.
Useful? React with 👍 / 👎.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
The watch hears Now when what it shows moves and otherwise once a second; its trace adds a point only when a value moved or a second passed. A reading that lands mid-slide slides on from where the head is drawn. Comments that assumed one frame a second say what holds now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The iPhone and watch half of sending live numbers as they land. Paired with srcfl/ftw#1649, where the box sends a frame on each driver poll through FTW Cloud instead of on its one-second clock. Either can merge first: this app works the same against a box that still sends once a second.
Before and after
A reading's trip from the box's driver to this app's
Sessiondecode, against a real localftwclouddand the box in process (srcfl/ftw#1649 has the method and every number):Launch to first live number, tunnel only: loopback p50 20 ms; at 50/20 ms round trips p50 276–296 ms, p95 283–299 ms over two runs.
What the app had to change
The box now sends a frame on every driver poll, up to five a second (three at once), and a beat each second when no poll comes; hidden stays at one frame in five seconds. I checked every rule that reads frame timing:
Source.state, from that source'sstaleAfterMs), andSiteModel.streamQuietAfterMs(3 s) is a carrier rule: three missed beats. The box still beats at least every 1.25 s, so a quiet house never reads as a lost session. Nothing grades a source from frame spacing.foregroundFrameDeadlineMs(2.5 s) holds: aftersub hz:1the next poll goes at once, and a beat within 1.25 s.LiveTrace's ring assumed 1 Hz. It took every traced field on every frame, 600 points, "ten minutes at 1 Hz". Now an unchanged value repeats at most once a second (repeatMs), as at 1 Hz, so the curve and its runs look the same, and the ring holds 1 500 points: five minutes at the box's fastest.WatchBridgeforwarded every frame, up to five a second. It now sends Now when what the wrist shows moves (a figure asPowerFormatprints it, a word, a level, the glow, "in control"), and otherwise once a second (WatchShare.worthSending).WatchTraceadds a point only when a value or its sight moved or a second passed (LiveSample.adds, the ring's rule), and builds the trace again only then.TraceFrame(series:after:arrivedAt:)starts the new slide where the head is drawn now (LiveSeries.midSlide), on the phone and the watch.CLAUDE.md(lane 0 is byte-identical in length; the constant cadence is the relay's),TunnelCarrier,SiteModel(34, 50, 484),Transport.swift's replay window (64 frames is about 13 s at five a second; the box's link holds a paced bulk frame at most 9 s, which srcfl/ftw tests), the ring file's size.LiveLatencyTestsis the phone half of the measurement, skipped unlessFTW_LIVE_LATENCY_DIRnames the box's directory (srcfl/ftwTestLiveBoxForTheiPhone). It logs each decoded grid reading with the wall clock, then times ten cold opens.TLS
URLSession resumed none of 54 TLS connections to a local
ftwcloudd, with a session per socket asURLSessionWebSocketdoes, one shared ephemeral session, or one default session, although the service issues tickets and a Go client resumes against it. The local test trusts a private CA through the session delegate, which may be why; with production's public certificate it is unproven. It costs no time: TLS 1.3 takes one round trip either way (53–58 ms at 50 ms RTT).Proof
aFrameThatCarriesAnotherDevicesReadingAddsNoPoint,theRingHoldsFiveMinutesAtTheFastestAndNoMore(NodeTests);aRepeatedValueAddsAPointOnlyOnceASecond,aReadingMidSlideSlidesOnFromWhereTheHeadIs(LiveOnArrivalTests);theWatchHearsAMoveAtOnceAndOtherwiseOnceASecond(WatchShareTests).swift test: 687 tests green. The app, the watch app and its widgets build for the simulator.LiveOnArrivalTests.🤖 Generated with Claude Code