Skip to content

Fix state freshness and make snapshots atomic - #8

Merged
justagist merged 6 commits into
mainfrom
freshness-snapshots
Oct 15, 2026
Merged

justagist merged 6 commits into
mainfrom
freshness-snapshots

Conversation

@justagist

@justagist justagist commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner
  • TimedFreqState compared seconds against a millisecond constant, so is_healthy stayed true
    for ~1000 s. It now uses steady_clock time points, with a per-state timeout: 1 s for the
    periodic feedback streams, none for query responses and command echoes.
  • StateSnapshot read value, timestamp, hz, is_valid and is_healthy through five separate
    locks; snapshot() now takes them under one.
  • disconnectPort() clears every cached state, so a new session cannot read old feedback.

Six new tests.

Summary by Sourcery

Ensure interface state freshness, snapshot consistency, and session isolation are handled correctly.

Bug Fixes:

  • Correct state freshness tracking so periodic feedback becomes unhealthy after its configured timeout while one-shot responses remain healthy after receipt.
  • Clear all cached interface state when disconnecting to prevent feedback from a previous session being exposed as current.

Enhancements:

  • Make state snapshots atomically capture payload and all associated metadata from one update.

Documentation:

  • Clarify state validity and health semantics for periodic feedback, query responses, and command echoes.

Tests:

  • Add coverage for health expiry, no-timeout responses, multipart freshness, atomic snapshots, reset behavior, and disconnect state clearing.

Copilot AI balanced review requested due to automatic review settings October 15, 2026 10:34
@sourcery-ai

sourcery-ai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

The PR fixes stale-health detection by using steady-clock arrival times with per-state policies, makes snapshots atomic across value and metadata, clears all cached state on disconnect, and adds targeted tests for freshness, consistency, and session boundaries.

Sequence diagram for fresh state snapshots

sequenceDiagram
    participant CAN as CAN read thread
    participant State as TimedFreqState
    participant Reader as Consumer
    CAN->>State: set(newValue, timestamp, caller_id)
    Note over State: Records steady_clock arrival time
    Reader->>State: snapshot()
    State-->>Reader: value, timestamp, hz, is_valid, is_healthy
Loading

Sequence diagram for disconnect state reset

sequenceDiagram
    participant Client
    participant Interface as PiperInterfaceV2
    participant Reader as CAN read thread
    participant States as Cached states
    Client->>Interface: disconnectPort()
    Interface->>Reader: stop and join
    Interface->>States: reset()
    States-->>Interface: Cached feedback cleared
    Interface-->>Client: Disconnected
    Client->>Interface: connectPort()
    Interface-->>Client: New session starts with invalid cached state
Loading

File-Level Changes

Change Details Files
Correct freshness tracking and make health evaluation configurable per state.
  • Track arrivals with steady_clock time points instead of epoch-seconds values.
  • Apply a one-second timeout to periodic feedback and disable expiry for one-shot responses and echoes.
  • Preserve multipart validity and require every registered part to remain fresh.
piper_cpp/include/piper_cpp/interface/timed_freq_state.h
piper_cpp/src/interface/piper_interface_v2.cpp
Make state snapshots internally consistent by reading all fields under one lock.
  • Add a locked snapshot path that computes value, timestamp, frequency, validity, and health from one state view.
  • Retain the existing snapshot API while routing it through the atomic implementation.
piper_cpp/include/piper_cpp/interface/timed_freq_state.h
Clear cached interface state whenever the port disconnects.
  • Reset every cached feedback, query, command, and health state after stopping the read thread and closing the CAN port.
  • Document that validity is session-scoped and stale feedback is unavailable after disconnect.
piper_cpp/include/piper_cpp/interface/piper_interface_v2.h
piper_cpp/src/interface/piper_interface_v2.cpp
Add coverage for timeout, multipart freshness, atomic snapshots, reset behavior, and session isolation.
  • Add focused TimedFreqState tests for expiring and non-expiring health, multipart updates, snapshot consistency, and reset semantics.
  • Add an interface test verifying disconnect invalidates and clears cached joint feedback.
  • Register the new test target with CMake.
piper_cpp/test/timed_freq_state_test.cpp
piper_cpp/test/piper_interface_v2_test.cpp
piper_cpp/test/CMakeLists.txt

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

@coderabbitai

coderabbitai Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: aa7a4eae-367f-42c9-9038-84001c4b3532
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@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 - I've found 6 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="piper_cpp/src/interface/piper_interface_v2.cpp" line_range="134" />
<code_context>
     }
     arm_can_->close();
     connected_.store(false, std::memory_order_relaxed);
+    resetStates();
+}
+
</code_context>
<issue_to_address>
**New feedback gets erased**

When `connectPort()` runs after `disconnectPort()` stores `connected_` false but before its `resetStates()` call completes, `connectPort()` starts a new reader while `disconnectPort()` is still resetting state, so `resetStates()` erases feedback received from the new session and callers see invalid or incomplete state.

Synchronize disconnect and reconnect so the old session's reset completes before the new reader can start.

Also at `piper_cpp/src/interface/piper_interface_v2.cpp:137`.
</issue_to_address>

### Comment 2
<location path="piper_cpp/src/interface/piper_interface_v2.cpp" line_range="157" />
<code_context>
+    arm_gripper_ctrl_.reset();
+    arm_motion_ctrl_code_151_.reset();
+    arm_instruction_response_.reset();
+    arm_health_.reset();
 }

</code_context>
<issue_to_address>
**Previous firmware data persists**

When firmware bytes were cached before disconnect and the interface reconnects or firmware data is read after disconnect, `resetStates()` leaves `firmware_data_` intact, so `getFirmwareData()` exposes old bytes and later responses append to them; `getFirmwareVersion()` can select the old first `S-V` marker and return the previous session's version.

Clear `firmware_data_` in `resetStates()` so firmware data and version queries cannot use bytes from a closed session.
</issue_to_address>

### Comment 3
<location path="piper_cpp/src/interface/piper_interface_v2.cpp" line_range="45" />
<code_context>

+    // Query responses and command echoes arrive once, not periodically, so their age says
+    // nothing about the link; only the periodic feedback streams keep the default timeout.
+    arm_current_end_vel_acc_.setHealthTimeout(kNoHealthTimeout);
+    arm_crash_protection_rating_fb_.setHealthTimeout(kNoHealthTimeout);
+    arm_gripper_teaching_pendant_fb_.setHealthTimeout(kNoHealthTimeout);
</code_context>
<issue_to_address>
**Stale feedback stays healthy**

When periodic end-velocity/acceleration feedback is enabled and its updates stop, `arm_current_end_vel_acc_` has `kNoHealthTimeout`, so its `is_healthy` flag remains true after updates stop and callers cannot detect stale feedback.

Keep a finite health timeout for `arm_current_end_vel_acc_` instead of setting `kNoHealthTimeout`.
</issue_to_address>

### Comment 4
<location path="piper_cpp/include/piper_cpp/interface/timed_freq_state.h" line_range="157" />
<code_context>
+#include <vector>

 namespace piper_cpp
 {

+template <typename T> struct StateSnapshot;
</code_context>
<issue_to_address>
**Header can fail to compile**

When the header is compiled with a standard library that does not transitively declare `std::move` from those includes, `StateSnapshot` calls `std::move` without including `<utility>`. A standard-library implementation that does not expose `std::move` through the other included headers fails to compile this header and its consumers.

Include `<utility>` directly in `timed_freq_state.h`.
</issue_to_address>

### Comment 5
<location path="piper_cpp/test/timed_freq_state_test.cpp" line_range="84-89" />
<code_context>
+    for (int i = 0; i < 20000; ++i)
+    {
+        const StateSnapshot<Reading> snap(state);
+        if (snap.is_valid && static_cast<double>(snap.value.n) != snap.timestamp)
+            ++mismatches;
+    }
+    stop.store(true);
+    writer.join();
+    EXPECT_EQ(mismatches, 0);
+}
+
</code_context>
<issue_to_address>
**Invalid snapshots pass consistency check**

When `snapshot()` returns an invalid/default snapshot on every call, `SnapshotPairsValueWithItsOwnMetadata` skips every comparison at line 84, and the zero-mismatch assertion at line 89 passes even though no snapshot exposes the updated state.

Assert that at least one snapshot is valid before accepting the consistency check.
</issue_to_address>

### Comment 6
<location path="piper_cpp/test/timed_freq_state_test.cpp" line_range="30" />
<code_context>
+    TimedFreqState<Reading> state;
+    state.setHealthTimeout(50ms);
+    state.set(Reading{1}, 0.0);
+    EXPECT_TRUE(state.isHealthy());
+
+    std::this_thread::sleep_for(80ms);
</code_context>
<issue_to_address>
**Timeout tests can fail after a pause**

When the test thread is descheduled for more than 50 ms between an update and either healthy assertion, `HealthExpiresAfterTheTimeout` fails its healthy assertion at line 30, or `MultipartHealthNeedsEveryPartFresh` fails at line 63, if the test thread is descheduled for over 50 ms after an update; the correctly implemented timeout then expires before the assertion runs.

Use a controllable clock for timeout tests so advancing time does not depend on thread scheduling.

Also at `piper_cpp/test/timed_freq_state_test.cpp:63`.
</issue_to_address>

Sourcery assessment

Approval pending. 6 findings to address first.

Blocking findings: piper_cpp/src/interface/piper_interface_v2.cpp:134, piper_cpp/src/interface/piper_interface_v2.cpp:157, piper_cpp/src/interface/piper_interface_v2.cpp:45, piper_cpp/include/piper_cpp/interface/timed_freq_state.h:157, piper_cpp/test/timed_freq_state_test.cpp:89, and 1 more


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment thread piper_cpp/src/interface/piper_interface_v2.cpp
Comment thread piper_cpp/src/interface/piper_interface_v2.cpp
Comment thread piper_cpp/src/interface/piper_interface_v2.cpp Outdated
Comment thread piper_cpp/include/piper_cpp/interface/timed_freq_state.h
Comment thread piper_cpp/test/timed_freq_state_test.cpp Outdated
Comment thread piper_cpp/test/timed_freq_state_test.cpp

Copilot AI 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.

🟡 Changes recommended

The firmware response cache survives disconnect and can expose an old version after reconnecting.

1 open finding
What changed in this PR

Improves CAN feedback freshness, snapshot consistency, and session isolation in the C++ interface.

Changes:

  • Adds per-state health timeouts and atomic snapshots.
  • Resets cached states on disconnect and documents the behavior.
  • Adds six tests covering freshness, snapshots, and resets.
File Description
piper_cpp/​test/​timed_freq_state_test.cpp Tests freshness, atomic snapshots, and reset behavior.
piper_cpp/​test/​piper_interface_v2_test.cpp Tests cache clearing on disconnect.
piper_cpp/​test/​CMakeLists.txt Registers the new state tests.
piper_cpp/​src/​interface/​piper_interface_v2.cpp Configures timeouts and resets session state.
piper_cpp/​include/​piper_cpp/​interface/​timed_freq_state.h Implements freshness tracking, atomic snapshots, and reset.
piper_cpp/​include/​piper_cpp/​interface/​piper_interface_v2.h Documents state semantics and declares reset helper.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread piper_cpp/src/interface/piper_interface_v2.cpp
@justagist
justagist merged commit a231466 into main Oct 15, 2026
5 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.

2 participants