Skip to content

Commit 65217f8

Browse files
committed
feat(notecard): Add StaticNotecard::ping() + parameterize seed source
Three connected changes that round out the ping work: 1. `StaticNotecard::ping()` lands as a mirror of `Notecard::ping()`, so the user-facing surface is the same on both Notecard variants. Important for the AVR / singleton path, where StaticNotecard is the only handle the application sees. 2. Both implementations are now **stateless**. The 16-character nonce is derived from a fresh xorshift32 seed per call, rather than from a PRNG state stored on the Notecard. The previous design stored a `uint32_t ping_prng_` member that the linker could not remove even when no caller invoked `ping()` — every `StaticNotecard` instance paid 4 bytes of RAM and the constructor paid ~8 bytes of flash for zero-initialising it, whether the feature was used or not. 3. Both implementations accept an optional `PingSeedFn` parameter that supplies the seed. The default is `hal().millis()`, which is what production callers want — millis advances between calls so consecutive nonces differ naturally on real hardware. Tests can inject a deterministic seed function instead so consecutive nonces are predictable. Surface details: - `using PingSeedFn = uint32_t(*)();` lives in `note::` namespace (defined in `include/note/notecard.hpp`, visible to both Notecard and StaticNotecard since static_notecard.hpp already includes notecard.hpp). Function-pointer shape rather than template / std::function — keeps AVR-class targets free of template explosion and `<functional>` baggage. - `Result<void> Notecard::ping(uint32_t timeout_ms = 500, PingSeedFn seed_fn = nullptr);` - `Result<void> StaticNotecard::ping(...)` — same signature. - `note::NotecardApi::ping(...)` pass-throughs updated on both the C++20 TargetT template and the legacy form. - The seed (user-supplied or millis) is XORed with the xorshift32 seed constant `0x2545F491u` so a caller that returns 0 (or a just-booted clock that has not advanced from 0) still produces a non-zero xorshift state. Test update: - `tests/test_ping.cpp` "successive calls produce different nonces" renamed to "different seeds produce different nonces" and now injects a counter-based seed function. Under the mocked transport `hal().millis()` returns 0, so without the injection two consecutive calls would share a nonce — the test now explicitly documents and verifies that two distinct seeds produce two distinct nonces. AVR size verification: avr-notecpp flash +0 ram +0 avr-notecpp-direct flash +0 ram +0 avr-notecpp-raw flash +0 ram +0 avr-notecpp-multistack flash +0 ram +0 Zero cost on every env — the `ping()` template method is correctly DCE'd by the linker when no caller instantiates it, and no member is added to the class either. The `+8 B / +4 B` cost from the stateful first cut is fully recovered. CHANGELOG: Unreleased section gains an entry describing the new `nc.ping()` surface, available on both `note::Notecard` and `note::StaticNotecard`. Host: 1877 cases pass. Arduino: 1894 cases pass.
1 parent cfd2094 commit 65217f8

5 files changed

Lines changed: 91 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ details belong in git commit messages and design docs, not here.
1010

1111
## [Unreleased]
1212

13+
### Added
14+
- `nc.ping()` — a one-shot connectivity probe that sends a nonce-bearing `echo` request and confirms the Notecard echoes the same nonce back. The call is deliberately stripped down: a single attempt, a short fixed default timeout (500 ms), and no retry, CRC, or transport reset on failure. The probe is safe to call at any point in the lifecycle, including before any other transaction has run. Available on both `note::Notecard` (polymorphic) and `note::StaticNotecard` (singleton / AVR). See [troubleshooting.md](docs/troubleshooting.md#im-getting-no-response-from-the-notecard) for guidance on using it as a triage step.
15+
1316
## [0.2.0] - 2026-04-21
1417

1518
### Added

src/note/notecard.hpp

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,15 @@
2323

2424
namespace note {
2525

26+
/// Optional seed-supplying function for `Notecard::ping()` /
27+
/// `StaticNotecard::ping()`. The probe's 16-character nonce is derived
28+
/// from this seed via an internal xorshift32 PRNG; passing `nullptr`
29+
/// (the default) seeds from the HAL clock (`hal().millis()`), which is
30+
/// what production callers want. Tests inject a deterministic seed
31+
/// function so consecutive nonces are predictable.
32+
using PingSeedFn = uint32_t(*)();
33+
34+
2635
#if NOTE_SINGLETON
2736
namespace detail {
2837
/// Shared NcT* (type-erased to void*) for all `request_traits<T>` under
@@ -704,25 +713,28 @@ class Notecard {
704713
/// safe to call at any point in the lifecycle, including before any
705714
/// other transaction has run.
706715
///
716+
/// The 16-character nonce is generated by an xorshift32 PRNG seeded
717+
/// from `seed_fn()` (or, when null, from the HAL clock). Tests can
718+
/// inject a deterministic seed function to make consecutive nonces
719+
/// predictable; production callers should leave `seed_fn` at its
720+
/// default so consecutive pings carry different nonces.
721+
///
707722
/// Returns success when the nonce matches; a transport error when
708723
/// the transport itself failed; or `Error::Json` when the response
709724
/// is missing a `text` field or its `text` value does not match the
710725
/// sent nonce.
711-
Result<void> ping(uint32_t timeout_ms = 500) {
726+
Result<void> ping(uint32_t timeout_ms = 500, PingSeedFn seed_fn = nullptr) {
712727
if (!transport_)
713728
return make_error(Error::NotReady, NOTE_ERR("no transport configured"));
714729

715-
if (ping_prng_ == 0)
716-
ping_prng_ = 0x2545F491u ^ hal().millis();
730+
uint32_t seed = (seed_fn ? seed_fn() : hal().millis()) ^ 0x2545F491u;
717731

718732
char nonce[16];
719733
for (int i = 0; i < 16; ++i) {
720-
uint32_t x = ping_prng_;
721-
x ^= x << 13;
722-
x ^= x >> 17;
723-
x ^= x << 5;
724-
ping_prng_ = x;
725-
nonce[i] = static_cast<char>('A' + (x % 26));
734+
seed ^= seed << 13;
735+
seed ^= seed >> 17;
736+
seed ^= seed << 5;
737+
nonce[i] = static_cast<char>('A' + (seed % 26));
726738
}
727739

728740
// Hand-build the request so we do not pull in snprintf on AVR.
@@ -1313,11 +1325,6 @@ class Notecard {
13131325
TransactionTiming timing_{};
13141326
uint32_t next_request_id_ = 1;
13151327
bool request_ids_enabled_ = true;
1316-
// xorshift32 state for the ping() nonce generator. Zero means
1317-
// "uninitialised"; ping() seeds it lazily from the host clock on
1318-
// first use so two Notecards constructed in quick succession do
1319-
// not share a sequence.
1320-
uint32_t ping_prng_ = 0;
13211328
#if !NOTE_NO_MD5
13221329
// Static, shared across all Notecards: avoids a self-reference inside
13231330
// Notecard (pointer to own member), which broke every move-assignment

src/note/notecard_api.hpp

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,9 +141,10 @@ class NotecardApi : private detail::NcOwner, public Api<TargetT> {
141141
}
142142

143143
/// One-shot `echo` connectivity probe. Forwarded to Notecard::ping;
144-
/// see the description there for the wire shape and timing.
145-
Result<void> ping(uint32_t timeout_ms = 500) {
146-
return nc_.ping(timeout_ms);
144+
/// see the description there for the wire shape, timing, and the
145+
/// meaning of `seed_fn`.
146+
Result<void> ping(uint32_t timeout_ms = 500, PingSeedFn seed_fn = nullptr) {
147+
return nc_.ping(timeout_ms, seed_fn);
147148
}
148149

149150
Notecard& notecard() { return nc_; }
@@ -203,8 +204,8 @@ class NotecardApi : private detail::NcOwner, public Api<Notecard> {
203204
}
204205

205206
/// One-shot `echo` connectivity probe — see C++20 overload above.
206-
Result<void> ping(uint32_t timeout_ms = 500) {
207-
return nc().ping(timeout_ms);
207+
Result<void> ping(uint32_t timeout_ms = 500, PingSeedFn seed_fn = nullptr) {
208+
return nc().ping(timeout_ms, seed_fn);
208209
}
209210

210211
Notecard& notecard() { return nc(); }

src/note/static_notecard.hpp

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,56 @@ class StaticNotecard {
308308
timeout_ms);
309309
}
310310

311+
/// One-shot `echo` connectivity probe. Mirror of `Notecard::ping()` —
312+
/// see the description there for the wire shape, timing, error
313+
/// semantics, and the meaning of `seed_fn`. The two implementations
314+
/// are deliberately kept in sync so the user-facing surface is the
315+
/// same on both Notecard variants.
316+
Result<void> ping(uint32_t timeout_ms = 500, PingSeedFn seed_fn = nullptr) {
317+
uint32_t seed = (seed_fn ? seed_fn() : stack_.transport.hal().millis()) ^ 0x2545F491u;
318+
319+
char nonce[16];
320+
for (int i = 0; i < 16; ++i) {
321+
seed ^= seed << 13;
322+
seed ^= seed >> 17;
323+
seed ^= seed << 5;
324+
nonce[i] = static_cast<char>('A' + (seed % 26));
325+
}
326+
327+
constexpr char kPrefix[] = R"({"req":"echo","text":")";
328+
constexpr char kSuffix[] = R"("})";
329+
constexpr size_t kPrefixLen = sizeof(kPrefix) - 1;
330+
constexpr size_t kSuffixLen = sizeof(kSuffix) - 1;
331+
char req[kPrefixLen + 16 + kSuffixLen];
332+
memcpy(req, kPrefix, kPrefixLen);
333+
memcpy(req + kPrefixLen, nonce, 16);
334+
memcpy(req + kPrefixLen + 16, kSuffix, kSuffixLen);
335+
336+
#if !NOTE_NO_RETRY
337+
enforce_timing();
338+
#endif
339+
char rsp_buf[64];
340+
auto rv = stack_.transport.transact_raw(string_view(req, sizeof(req)),
341+
rsp_buf, sizeof(rsp_buf),
342+
timeout_ms);
343+
#if !NOTE_NO_RETRY
344+
record_timing();
345+
#endif
346+
if (!rv) return Unexpected(rv.error());
347+
348+
string_view rsp = *rv;
349+
constexpr string_view key = R"("text":")";
350+
auto pos = rsp.find(key);
351+
if (pos == string_view::npos)
352+
return make_error(Error::Json, NOTE_ERR("ping: response missing text field"));
353+
pos += key.size();
354+
if (pos + 16 > rsp.size())
355+
return make_error(Error::Json, NOTE_ERR("ping: response text too short"));
356+
if (memcmp(rsp.data() + pos, nonce, 16) != 0)
357+
return make_error(Error::Json, NOTE_ERR("ping: nonce mismatch"));
358+
return Result<void>{};
359+
}
360+
311361
private:
312362
using InplaceBuilder = void(*)(JsonRender& w, const void* ctx);
313363

tests/test_ping.cpp

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ TEST_CASE("ping: propagates transport errors") {
8888
CHECK_FALSE(rv);
8989
}
9090

91-
TEST_CASE("ping: successive calls produce different nonces") {
91+
TEST_CASE("ping: different seeds produce different nonces") {
9292
std::string first_request;
9393
std::string second_request;
9494
int call = 0;
@@ -104,9 +104,17 @@ TEST_CASE("ping: successive calls produce different nonces") {
104104
return string_view(rsp_buf, static_cast<size_t>(n));
105105
});
106106

107+
// The default seed is hal().millis(), which is 0 under the mocked
108+
// transport, so two consecutive calls would otherwise share a nonce.
109+
// Inject a counter-based seed to make consecutive nonces predictable
110+
// and confirm they differ.
111+
static uint32_t counter;
112+
counter = 1;
113+
auto seed = []() -> uint32_t { return counter++; };
114+
107115
Notecard nc(nullptr, t);
108-
auto a = nc.ping();
109-
auto b = nc.ping();
116+
auto a = nc.ping(500, seed);
117+
auto b = nc.ping(500, seed);
110118
REQUIRE(a);
111119
REQUIRE(b);
112120
auto first_nonce = extract_nonce(first_request);

0 commit comments

Comments
 (0)