serve() now replies with Response::Error on a malformed JSON frame and
keeps the connection serving instead of dropping it on the parse error;
the per-connection loop only breaks on a clean disconnect
(UnexpectedEof). node.lock().unwrap() is replaced with poison-safe
into_inner() so one panicking handler can't take down the daemon.
Drop duplicate tines-core/tines-store/serde/serde_json from
dev-dependencies (only heapless is genuinely test-only).
New `tines` crate (added to workspace members) hosting a `Node<LogStore,
BtTransport>` and serving a tiny request/response IPC over a Unix-domain
socket for robots without ROS2. Four operations: `observe`, `belief`,
`events`, and `status`, over length-prefixed JSON frames.
The `serve` loop now serves each connection request-by-request (previously
it handled one request per connection, dropping the socket after the first
response). Integration tests drive `handle`/`serve` with a mock transport
that auto-acks observed broadcasts and echoes an empty Summary, so a
confidence event fires without a real BLE radio.
Adds the ninth fleet-deployment crate. Default build is ROS2-free: topic_name
hex-escapes arbitrary tines topics into valid ROS2 names under /tines/ (deterministic,
injective) and wires Node<LogStore, BtTransport>. The rclrs-backed self-contained
TinesNode lives under #[cfg(feature="ros2")] with an off-by-default ros2 feature, so
the crate builds/tests without a ROS2 toolchain. Adds scripts/check-ros2.sh (needs a
ROS2 toolchain; not run in CI), registers the crate in workspace members, and passes
sh scripts/ci.sh.
Task 8 of the fleet-deployment plan. Adds the tines-bt crate: a real
`Transport` over the `bluer` crate (D-Bus BlueZ).
- `Addr` = peer's advertised `NodeId`, so identity survives BLE
address rotation; advertising payload (flags + 16-bit service UUID +
NodeId in service data) fits legacy 31-byte EIR (`advertise()`).
- `BtTransport` drops any inbound frame whose `from` differs from the
connection's advertised NodeId (protocol violation at the transport;
core never sees a mismatch).
- Connection symmetry: the node with the numerically smaller NodeId is
the BLE central/initiator (`is_central`), independent of which side
opens the radio.
- `mtu()` reports the real L2CAP CoC MTU (falling back to the ATT MTU);
the transport does NOT split frames — core folds summaries/fragments
to `mtu()`.
- Radio I/O sits behind the narrow `Radio` trait (a `poll`-driven event
stream), so the pure logic (payload assembly, mismatch-drop, central
rule) is unit-tested with a mock radio and never needs hardware.
- `BluerRadio` drives the stack on a detached tokio task: powers the
adapter, advertises, monitors for the service UUID, opens an L2CAP
byte pipe, and surfaces inbound bytes tagged with the peer NodeId.
Adds `tests/adv.rs` (8 mock-Radio integration tests covering the 31-byte
limit, NodeId round-trip, MTU clamp, mismatch-drop, per-frame MTU, and
central rule). Added to workspace members; workspace CI (fmt, doc,
test, clippy -D warnings) is green.
LogStore::new folded `(now - last_clock)` into every entry's age_ms, but
age_ms is persisted at INSERT time and stored_at was never caught up at
flush. So an observation inserted before the last flush lost the
`last_flush - insert_time` after restart (a store that was non-idle
between insert and flush reported an age too small by that gap).
Fix by anchoring each entry's age to its own insert tick: carry
`stored_at` on the Obs record and fold `(now - stored_at)` per-entry at
load, instead of a single global fold against the last clock marker.
flush() now also advances each in-memory age to its current value so the
representation stays coherent within a run.
This is exact for idle stores and observations inserted before the last
flush. Adds tests/persistence.rs::ages_rebase_after_nonidle_flush
(insert A at tick 1000, flush at 3000, reopen at 9000 -> asserts true
age 8100, not 6100).
Add the host-side std `tines-store` crate implementing the `tines_core::Store`
trait over a durable, append-only single-file log so a robot's memory survives
reboot.
- Log format: length-prefixed `[u32 LE][postcard]` records (Obs/Trim/Meta).
- On load the index is rebuilt by replaying the log, then every age is
rebased to load time: elapsed time since the last persisted clock marker is
folded into `age_ms` and `stored_at` reset to load `now`, so ages continue
instead of freezing across a reboot.
- Persisted state: released-id set, `next_seq`, and a clock marker in a `Meta`
record; dedupe to the oldest Obs and let the last event per id win so a
re-inserted id survives a trim.
- Implements `next_seq` and `flush`; `trim` writes Trim records best-effort.
- Registered as a workspace member; std-only, `#![forbid(unsafe_code)]`.
- Adds persistence tests (append durability, rebase-on-load, released/seq
persistence). Full gate (fmt, clippy, docs, tests, no_std thumb build) is green.
Buffer pending acks (acks for addressed, bcast_acks for broadcast,
ACK_BUFFER_CAP=256, overflow drops oldest), skip a released-duplicate
offer in accept(), and emit Ack frames in next_frame after a due summary
and before addressed/broadcast traffic, draining up to ACK_IDS_CAP ids
per destination and splitting across frames to fit mtu.
Release custody on an Ack only when the Ack confirms delivery to the
intended recipient: an Ack from a peer that merely holds an
epidemic-forwarded copy no longer releases custody, so the local copy
stays available to forward toward the recipient once the only contact to
it closes.
Symlink the root Apache-2.0 LICENSE into tines-core and tines-sim so
`cargo package` includes the license text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share version, edition, license, authors, repository and readme through
[workspace.package]; add per-crate documentation, keywords and
categories. Fixes `cargo publish` warning about missing documentation,
homepage or repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump tines-core and tines-sim to 0.2.0 (Peer::in_flight changed type).
Add CHANGELOG.md. README gains a "Writing a store" section and a note on
calling Exchange::on_send_error when driving Exchange without Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README: protocol, fragmentation, poll and addressed-delivery diagrams;
observation fields and identity; transport, belief and trim guidance;
simulator example; constants table.
AGENTS.md: drop the completed plan's task-by-task workflow and Task 14
notes, point at both specs, use scripts/ci.sh as the gate, and update
the identity-hash constraint to include `to`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add Store::for_each_addressed_id (default: every id; MemStore filters)
so custody selection no longer clones every broadcast observation on
each poll.
- Factor fragment sizing into frag::plan and MAX_FRAGMENTS, used by both
frag::fragment and the exchange, and by the reassembler's bounds.
- Document `to` in the Observation::id hash input, matching the
addressed-delivery plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cut every fragment of a stream from one encoding (age frozen at the
first fragment) and bounds-check the slice: re-encoding mid-stream
corrupted fragments and could panic when the tick went backwards.
- Fold the bloom summary to fit the MTU instead of sending a ~540-byte
frame that small-MTU links reject; drop the sim's summary MTU
exemption that hid this.
- Finish a peer's in-flight stream before starting another, and count
an addressed message as delivered only after its last fragment.
- Alternate custody and broadcast per peer, rotate through addressed
candidates, and evict the oldest bounce record when full, so neither
path starves.
- Add Exchange::on_send_error; Node::poll uses it so Busy/Unreachable
sends are retried rather than silently dropped.
- Clear bounce records per contact, record duplicates against their
sender, and skip addressed messages that cannot fit the MTU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run clippy, docs and tests for tines-core with --no-default-features,
gating the MemStore-based tests on `alloc` so they compile there. Fail on
rustdoc warnings, pass --locked everywhere, and pin the toolchain to
1.99.0. In the workflow, use read-only permissions, cancel superseded
runs, add a 20-minute timeout, and pin actions to commit SHAs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>