# Changelog
All notable changes to peat-btle will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.3.2] - 2026-05-03
ADR-059 Amendment 3 — host-side wiring shifts from UniFFI callback to
**polled struct field** on `DataReceivedResult`. Implementation-time
investigation against 0.3.1 surfaced that UniFFI 0.31's Kotlin backend
wraps `#[uniffi::export(callback_interface)]` traits in
`com.sun.jna.Callback` (verified against the regenerated peat_btle.kt
in 0.3.1's sources jar), and JNA-based Rust→Kotlin callbacks fail
under ATAK's classloader isolation (documented prior art for peat-ffi's
`OutboundFrameCallback`). Amendment 3 bypasses the callback shape
entirely — the plugin reads a new field on the existing
`DataReceivedResult` and forwards via peat-ffi's
`publishDocumentWithOriginJni`.
The two callback paths from 0.3.0 / 0.3.1 stay in the public API as
non-ATAK escape valves. Three independent dispatch paths now coexist:
Rust-trait callback (in-process Rust consumers), UniFFI JSON callback
(non-ATAK Rust integration tests), polled struct field (ATAK / every
UniFFI host).
### Added
- **`DecodedTranslatorFrame { collection, doc_json, peer }` UniFFI
record.** Defined unconditionally — *not* `#[cfg(feature = "mesh-translator")]`.
The type itself has no feature-dependent surface; gating it would
force the `decoded_translator_frame` field to also be gated and
break the binding-shape stability claim. Population happens only
when `mesh-translator` is on; without the feature, the field is
always `None`.
- **`decoded_translator_frame: Option<DecodedTranslatorFrame>` field on
`DataReceivedResult`** — both the inner `peat_mesh::DataReceivedResult`
and the UniFFI binding wrapper. Populated by the receive dispatch
when a 0xB6 translator frame decodes successfully. Hosts forward
populated entries through their existing publish-with-origin FFI
surface (e.g. peat-ffi's
`publishDocumentWithOriginJni(collection, doc_json, "ble")`).
- **`PeatMesh::acknowledge_polled_translator_consumer()` method** —
defined unconditionally for binding-shape stability. Hosts that
read `DataReceivedResult.decoded_translator_frame` per receive and
forward through their own publish path SHOULD call this once at
startup. Without the attestation, the receive dispatch fires
`PeatEvent::TranslatorNoCallback` for every decoded frame because
it has no way to distinguish a polled-consumer host from a host
that decoded the frame and dropped it on the floor. Idempotent —
calling again is a no-op. Backed by `AtomicBool`, also unconditional.
- **`TranslatorMarkerOutcome` enum return** for the internal
`try_handle_translator_marker` dispatch helper. Replaces the prior
`bool` return with `NotTranslatorMarker | Decoded(frame) | Handled`.
Per-call return-threaded — no shared mutable state, no `Mutex`, no
thread-local. Concurrent receives on different threads stay race-free.
- **Three new regression fixtures** in
`tests/reserved_marker_silent_drop.rs`:
- `polled_field_populated_on_decoded_translator_frame` — locks the
polled-field surfacing contract.
- `polled_attestation_suppresses_no_callback_event` — locks the
canonical polled-only deployment shape (peat-atak-plugin
startup): no callbacks, attestation set, `TranslatorNoCallback`
must not fire.
- `polled_attestation_is_idempotent` — locks the spec's "calling
again is a no-op" guarantee.
### Changed
- **Receive-entry-point return contract for 0xB6 frames.** In 0.3.0 /
0.3.1, `on_ble_data_received*` returned `None` for any
successfully-handled translator frame (the old "if
try_handle_translator_marker { return None; }" branch). In 0.3.2
the same path now returns `Some(DataReceivedResult { source_node,
decoded_translator_frame: Some(..), .. /* other fields default */
})`. Hosts updating to 0.3.2 **MUST** early-branch on
`result.decoded_translator_frame.is_some()` before reading any
other fields on the returned result — counter_changed,
total_count, callsign, etc. are all defaulted on the polled-only
path, and `source_node` is `NodeId::new(0)` for the anonymous
receive entry point (no PeatDocument header to extract from).
Hosts that gated on `result.is_some()` as "real sync data
arrived" without further inspection will see that branch fire
on every successfully-decoded translator frame in 0.3.2 — read
the new field first, treat it as the polled-output path, and
skip the legacy CRDT-field interpretation when populated.
- **`PeatEvent::TranslatorNoCallback` suppression rule extended to
include the polled-consumer attestation.** Event fires when **all
three** conditions hold: no Rust-trait callback installed, no
UniFFI JSON callback installed, and
`acknowledge_polled_translator_consumer` was never called. Any one
of the three suppresses it. Preserves the 0.3.1 contract (the event
still surfaces "no Rust-side consumer registered interest") while
allowing the canonical 0.3.2 polled-only deployment to run without
per-frame events.
### Wire format
Unchanged. 0xB6 marker, collection-code byte, postcard payload — same
as 0.3.0+. The in-process API contract did change for the 0xB6 receive
path (see §Changed); the on-the-wire contract did not.
### Apple FFI scope (iOS / macOS via `peat-apple-ffi`)
`peat-apple-ffi` (at `ios/peat-apple-ffi/`) is a separate Cargo crate
with its own narrower `DataReceivedResult` and field-by-field
translation. It does not yet expose Amendment 3:
`decoded_translator_frame` is not in the Apple-side struct, no
`acknowledge_polled_translator_consumer` re-export, and the Apple
build does not enable the `mesh-translator` feature. The
binding-shape stability claim above scopes to peat-btle's direct
UniFFI bindings (the AAR consumed by peat-atak-plugin and other
Kotlin / Android UniFFI hosts). Swift / iOS consumers compiling
against `peat-apple-ffi` won't see Amendment 3 until the Apple FFI
crate is updated; tracked as a follow-up rather than blocking the
0.3.2 release since no Apple host actively consumes peat-btle's
translator path today.
### Android AAR
`android/build.gradle.kts` now passes `--features android,mesh-translator`
to `cargo build` (was `--features android`) so the published AAR
contains the receive-dispatch + polled-field code path. Required for
peat-atak-plugin and other UniFFI host consumers; standalone BLE
consumers (Bitchat-style apps, embedded sensors) compiling against
the Rust crate without the `mesh-translator` Cargo feature still see
no peat-mesh in their dep graph.
The Android AAR `version` field in `android/build.gradle.kts` also
bumps to 0.3.2 (was 0.1.1, never tracked the Cargo version). Maven
Central / Maven local artifacts now match the crates.io version.
## [0.3.1] - 2026-05-02
ADR-059 Amendment 2 (Slice 1.b.4 host-side wiring) — adds the
UniFFI-exported `DecodedDocumentJsonCallback` so Kotlin / Swift hosts
can install a callback for inbound translator frames and forward each
decoded doc into peat-ffi's `publishDocument` with `origin="ble"`.
Amendment 2 corrected Amendment 1's "Lands in peat-ffi … Registered at
PeatNode construction" claim: peat-ffi never owns a `peat_btle::PeatMesh`
instance, so the wiring lives in the host (peat-atak-plugin et al.)
where the existing UniFFI / JNI split already holds handles to both
peat-btle's BLE stack and peat-ffi's `PeatNode`.
### Added
- **`crate::DecodedDocumentJsonCallback` UniFFI callback trait**
(`#[uniffi::export(callback_interface)]`, gated on
`mesh-translator` + `uniffi`). Same signature as the Rust-only
`DecodedDocumentCallback` from 0.3.0 except the
`peat_mesh::sync::Document` payload is serialized to JSON before
invocation. Kotlin / Swift hosts implement this trait; the
receive-dispatch fires it alongside the Rust-trait callback so both
can be installed independently.
- **`PeatMesh::set_decoded_document_json_callback(Box<dyn DecodedDocumentJsonCallback>)`**
on both the inner `peat_mesh::PeatMesh` and the UniFFI `PeatMesh`
wrapper. Idempotent — replaces any prior installation. Internally
stores as `Arc<dyn ...>` so the receive dispatch can snapshot-clone
the handle out of the lock without holding it during user-supplied
dispatch (same pattern as `decoded_document_callback`).
- **Receive-dispatch dual-firing**: `try_handle_translator_marker`
invokes both callbacks (Rust-trait and UniFFI-JSON) when both are
installed, each independently of the other. JSON serialization
failures log + skip only the JSON path (the Rust-trait callback,
if installed, still fires on the typed `Document`).
- **`PeatEvent::TranslatorNoCallback` suppression rule**: the event
fires only when **both** callback slots are empty. Installing
either callback suppresses it. Documents the release-skew window
remains operator-observable while the JSON path is being rolled out.
- **Regression test**: `tests/reserved_marker_silent_drop.rs::json_callback_fires_and_suppresses_no_callback_event`
installs a UniFFI JSON callback, sends a 0xB6 platforms frame, and
asserts (a) the JSON callback received the doc with the correct
collection name + peer, (b) the JSON payload round-trips through
`serde_json::Value`, and (c) `TranslatorNoCallback` did not fire.
Locks the both-callbacks-fire-independently and
installed-suppresses-no-callback-event guarantees so a future
refactor can't silently drop them.
### Operational notes
- Hosts upgrading from 0.3.0 to 0.3.1 do not need to re-flash any
legacy peers — the wire format is unchanged. 0.3.1 is purely an
additive API surface release: existing `PeatMesh` consumers see no
behavioral change; consumers that install the new callback gain
cross-transport receive-side ingest.
- Slice 1.b.4 host work (peat-atak-plugin's `BleDecodedDocumentBridge`)
unblocks once 0.3.1 is on crates.io.
## [0.3.0] - 2026-05-01
ADR-059 Amendment 1 (Slice 1.b.3) — BLE receive-side wire format,
`DecodedDocumentCallback` trait, and translator-frame dispatch.
Required by peat-ffi Slice 1.b.4 to install the callback and thread
`Node::publish_with_origin(.., Some("ble"))` into the doc-store ingest
path. Closes the receive-side gap that ADR-059's "Inbound transport
bridge" pseudocode glossed over and unblocks the peat-atak-plugin
migration queue (#65 / #66 / #69 / #26 in that repo).
### Added
- **`mesh-translator` feature** (default-off, ADR-059): ships
`BleTranslator` + the `peat_mesh::transport::Translator` impl so a
peat-mesh-using consumer can plug peat-btle into
`peat_mesh::transport::TransportManager` for cross-transport document
bridging. Standalone BLE consumers (Bitchat-style apps, embedded
sensors) compile peat-btle without the feature and never pull
peat-mesh, serde_json, postcard, anyhow, or tracing into their dep
graph. **peat-mesh is exact-pinned to `=0.9.0-rc.4`** to match the
peat workspace pin — this pin must move in lockstep with the next
peat-mesh release (peat-mesh release → peat workspace bump → peat-btle
Cargo.toml bump → peat-btle release), otherwise downstream consumers
pulling both peat and peat-btle with `mesh-translator` enabled will
hit a resolver conflict.
- **ADR-059 Amendment 1 wire format** (`mesh-translator` feature):
`TRANSLATOR_FRAME_MARKER = 0xB6` on the BLE wire, with a 1-byte
collection code (`COLLECTION_CODE_TRACKS = 0x01`,
`COLLECTION_CODE_PLATFORMS = 0x02`, `COLLECTION_CODE_ALERTS = 0x03`,
`COLLECTION_CODE_CANNED_MESSAGES = 0x04`) and a postcard-encoded
payload. Codes are immutable. 0xB6 sits above the ADR-001
trust-architecture reservations (0xB3–0xB5 IDENTITY_ATTESTATION /
REVOCATION / KEY_ROTATION) and below the 0xC0–0xCF `DocumentType`
registry range. 0xB7–0xBF are reserved for future translator-frame
variants and are silent-dropped on receive.
- **Symmetric encode/decode**: `BleTranslator::encode_outbound` now
emits the fully-framed wire bytes; `decode_inbound` (and the new
sync helper `decode_inbound_sync`) take payload-only bytes after
peat-btle's receive dispatch strips marker + code. peat-btle is the
single source of truth for the wire format in both directions —
hosts (peat-ffi → peat-atak-plugin / wearos-tak-civ / ...) own no
per-translator-frame logic and forward bytes through unchanged.
- **`DecodedDocumentCallback` trait** + `MeshDocument` re-export:
invoked by peat-btle's GATT receive after a 0xB6 frame decodes
successfully. peat-ffi (Slice 1.b.4) installs the callback and calls
`Node::publish_with_origin(collection, doc, Some("ble"))` from inside
it so origin threading prevents BLE echo in cross-transport fan-out.
- **`PeatMesh::set_decoded_document_callback`** + **`set_translator_config`**:
setters for the receive-side callback and translator config (all
gated by `mesh-translator`). The callback is `None` until installed —
the deliberate Slice 1.b.3-ships-before-1.b.4 release-skew window.
- **`PeatEvent::TranslatorNoCallback { collection, peer }`**: emitted
on the existing observer channel when a 0xB6 frame decodes
successfully but no `DecodedDocumentCallback` is installed. Lets
operators staging the rollout see the no-callback gap in real time
without depending on `log::debug!` (disabled at default log levels).
- **Reserved-marker rejection** in `on_ble_data_received_anonymous`
and identified-peer siblings: first-byte ∈ 0xB7..=0xBF is silent-
dropped *before* fall-through to `merge_document`. Required to
prevent the GCounter-pollution hazard documented in ADR-059
Amendment 1 §"Backwards compatibility… 2" (a 0xB6+ payload whose
bytes 8..11 fall ≤ `MAX_COUNTER_ENTRIES = 256` would otherwise be
accepted as a valid-but-garbage `PeatDocument`; the (0,0)+None+None
position broadcast is the concrete reachable instance).
**Operational mandate**: deployments must roll out a peat-btle
version with this rejection check to every BLE peer **before** any
peer is upgraded to a version that emits 0xB6 frames. Pre-0.3.0
peers do not provide a guaranteed silent-drop; the rejection check
closes the hazard once rolled out.
- **Public marker constants** (`mesh-translator` feature):
`TRANSLATOR_FRAME_MARKER`, `TRANSLATOR_RESERVED_MARKER_START`,
`TRANSLATOR_RESERVED_MARKER_END` for in-tree dual-emit gating logic
and tests. **Not** for host use — outbound framing lives entirely
inside `BleTranslator::encode_outbound` per ADR-059 Amendment 1
§"Outbound framing".
### Fixed
- **`mesh-translator` tracks decode**: missing or unparsable
`ctx.local_wire_id` now returns `Err` instead of silently
substituting `peripheral_id = 0`. The previous fallback collapsed
every unknown-source BLE track onto `ble-00000000` — invisible from
the receiver's perspective. The Err lets the inbound bridge's
parse-error metric surface the misconfiguration loudly.
### Wire-format coordination
- 0.3.0 is the first peat-btle release that emits and recognizes 0xB6
translator frames. Mixed-fleet rollouts see the dual-emit transition
the amendment specifies: hosts that publish via the
`mesh-translator` path emit *both* the 0xB6 translator frame and the
matching legacy `PeatDocument`-shaped frame for the same logical
doc, and Automerge merges the second receiver-side ingest as a
no-op. Pre-0.3.0 peers see legacy frames as before; post-0.3.0
peers see both. Operator-facing dual-emit toggle (`enable_legacy_emit`)
ships in a follow-up release once the orchestrator-in-loop test
fixtures land.
## [0.1.0] - 2026-02-12
### Added
- **Android**: High-priority sync mode for time-critical state updates
- **Android**: WearOS reliability improvements (reconnect on re-discovery, stale peer cleanup, address rotation handling, auto-reconnect with exponential backoff)
- **feather-sense**: Peat GATT server for WearTAK connectivity on nRF52840
- **feather-sense**: Pure Rust BLE advertising with nrf-sdc
- **feather-sense**: probe-rs build support and BLE target in Makefile
- **macOS**: GATT client with bidirectional sync
- BLE connection management infrastructure with UniFFI exports
- Range test node for WearTAK field testing
- Adafruit Feather Sense (nRF52840) example support
- CannedMessage CRDT document sync via delta mechanism
- Extensible document registry for CRDT sync
- nRF52840 sensor beacon example
### Fixed
- **Android**: Reconnect to peers when re-discovered after disconnect
- **Android**: Clean up stale connected peers and improve display names
- **Android**: Peer tracking with name-based deduplication
- **Android**: Handle WearOS BLE address rotation for stable mesh state
- **Android**: Prevent GATT server registration leaks
- **Android**: Prevent unwanted BLE pairing requests on Samsung devices
- **Apple**: CoreBluetooth memory management segfault
- **Linux**: BLE connection handling and advertisement improvements
- Relay deduplication and callsign cache
- CI checkout and format/clippy/macOS example gating fixes
### Changed
- **feather-sense**: Corrected GPIO P1 base address for raw_blinky
## [0.1.0-rc.30] - 2026-01-29
### Added
- `MembershipToken`: Lightweight authority-signed tokens (128 bytes) for constrained devices
- Binds public_key to callsign with mesh_id and expiration
- Wire format: `[pubkey:32][mesh_id:4][callsign:12][issued:8][expires:8][sig:64]`
- `SignedPayload`: Transport-agnostic signing utilities for BLE and WiFi/IP
- Wire format: `[marker:1][payload:N][signature:64]`
- `IdentityRegistry` extended with callsign support:
- `register_member()` validates and stores membership tokens
- `get_callsign()` / `find_by_callsign()` for lookups
- Persistence format v2 with backwards compatibility
## [0.1.0-rc.29] - 2026-01-27
### Added
- Functional BLE loopback test automation (kitlab ↔ Pi)
- `ble_responder` and `ble_test_client` example binaries
- Delta sync auto-registration on peer connect/disconnect
- `decrypt_only()` API for transport-only decryption
### Fixed
- UInt formatting in Android logs (`.toLong()` for `String.format`)
- Added `updatePeripheralState()` convenience method (ATAK team contribution)
### Changed
- CI now runs functional BLE test via SSH to Raspberry Pi
## [0.1.0-rc.28] - 2026-01-26
### Changed
- **BREAKING**: Migrated Android bindings from manual JNI to UniFFI
- All Rust types now accessed via `uniffi.peat_btle` package
- PeatMesh construction uses `newFromGenesis()` or `newWithPeripheral()` factory methods
- Method parameters now use Kotlin unsigned types (UInt, ULong, UByte)
- BLE callback timestamps require `.toULong()` conversion
### Added
- UniFFI bindings module (`src/uniffi_bindings.rs`) with full PeatMesh API
- Generated Kotlin bindings (`android/src/main/kotlin/uniffi/peat_btle/peat_btle.kt`)
- Chat methods exposed via UniFFI: `sendChat`, `sendChatReply`, `chatCount`, `getAllChatMessages`, `getChatMessagesSince`
- `updatePeripheralState` method for efficient encrypted state updates
- `deriveNodeIdFromMac` standalone function
- Peer state types via UniFFI: `ConnectionState`, `PeerConnectionState`, `StateCountSummary`, `FullStateCountSummary`, `IndirectPeer`, `ViaPeerRoute`
- Peer state methods: `getPeerConnectionState`, `getDegradedPeers`, `getLostPeers`, `getConnectionStateCounts`, `getIndirectPeers`, `getFullStateCounts`
### Fixed
- Removed JNI native method calls from callback proxies (`ScanCallbackProxy`, `GattCallbackProxy`, `AdvertiseCallbackProxy`) that caused `UnsatisfiedLinkError` at runtime
### Removed
- Manual JNI bridge (`src/platform/android/jni_bridge.rs`)
- JNI-based Kotlin files: `PeatMesh.kt`, `DeviceIdentity.kt`, `MeshGenesis.kt`, `IdentityAttestation.kt`
- JNI native method declarations from callback proxy classes
- `System.loadLibrary("peat_btle")` calls (UniFFI/JNA handles library loading automatically)
- `jni` and `ndk` crate dependencies
### Migration Guide
See `docs/UNIFFI_MIGRATION.md` for Android integration updates.
## [0.0.12] - 2026-01-19
### Added
- ADR-002: Mesh Provisioning and Node Onboarding architecture
- Codex.md with Radicle workflow guide and CI documentation
- Security implementation roadmap with 8 tracked issues
### Fixed
- Clippy warnings in linux adapter (derivable_impls, type_complexity, manual_strip, clone_on_copy)
- Range contains checks in CRDT validation
- linux_scanner example (rand dependency, callback types)
- Code formatting across multiple files
## [0.0.11] - 2026-01-18
### Added
- ChatCRDT for persistent mesh chat with reply threading
- Chat message deduplication in Android bindings
- MTU overflow protection (CHAT_SYNC_LIMIT=8)
- Profiling stress test example
### Fixed
- BLE MTU overflow crash with large chat histories
- Duplicate chat notifications in Android
## [0.1.0] - 2024-12-01
### Added
- Initial release
- Linux platform support with BlueZ/bluer
- Core BLE transport architecture
- BleAdapter trait for platform abstraction
- Peat beacon format and discovery protocol
- GATT service definition (0xF47A)
- Power profile management (Aggressive, Balanced, LowPower, UltraLow)
- Lightweight CRDT implementations (GCounter, LWWRegister, ORSet)
- Hierarchical mesh topology support
- Delta-based synchronization protocol
- Coded PHY support for extended range (BLE 5.0+)
### Platform Support
- Linux (BlueZ 5.48+) - Complete
- macOS (CoreBluetooth) - Complete
- iOS (CoreBluetooth) - Complete
- ESP32 (NimBLE) - Complete
- Android - In Progress
- Windows - Planned