Commit Graph
18 Commits
Author SHA1 Message Date
swaits 66a2484aeb ci: update fuzz target list to the v2 names
The Fuzz workflow step was still iterating the old v0.1 target names (parse_radio_config, cobs_decoder_feed, cobs_roundtrip, command_roundtrip, cobs_decoder_multi_frame, cobs_chunk_invariant). After the Protocol v2 rewrite only five targets exist: decoder_feed, frame_roundtrip, lora_config_roundtrip, parse_command, parse_device_message. Update the workflow to match.
2026-04-22 10:33:06 -06:00
swaits 9ec45e32b7 chore: update repository URL after GitHub rename
GitHub renamed the repo from donglora/protocol to donglora/protocol-rs (the Rust implementation is now distinguished from a forthcoming protocol-py sibling). Update Cargo.toml repository/homepage and README badge / link URLs so the published 1.0.0 crate points at the new location.
2026-04-22 09:24:01 -06:00
swaits 85420dc673 chore: release 1.0.0 2026-04-22 09:21:12 -06:00
swaits 4cccb65124 feat!: protocol v2 wire format and module restructure
The wire protocol is re-specified as DongLoRa Protocol v2 (see PROTOCOL.md). Every type_id, tag-correlation rule, and field layout is incompatible with 0.1.0.

* Add PROTOCOL.md as the normative spec for the v2 wire format.

* Replace framing.rs with a streaming FrameDecoder that emits (type_id, tag, payload) triples on each COBS-delimited, CRC-checked frame.

* Split the single-file lib.rs into dedicated modules: frame, commands, events, errors, modulation, info, crc, chip_id. Re-exports preserve convenience imports at the crate root.

* Replace RadioConfig / Bandwidth with a Modulation enum covering LoRa, FSK/GFSK, LR-FHSS, and FLRC, each with its own typed parameter struct.

* Add TxFlags, RxOrigin, SetConfigResult, OkPayload to model v2 response shapes, and RadioChipId for GET_INFO.

* Rewrite fuzz corpus: replace the 4 cobs_* / command_roundtrip targets with decoder_feed, frame_roundtrip, lora_config_roundtrip, and parse_device_message; drop parse_radio_config.

* Expand proptest and mutation-killer coverage for the new wire shape.

* Rewrite canonical-byte test vectors against the v2 spec.
2026-04-22 09:21:11 -06:00
swaits 35866edac4 ci: also run workflow on pushes to dev 2026-04-13 14:10:16 -06:00
swaits da9249446c ci: pin fuzz jobs to x86_64-unknown-linux-gnu
cargo-fuzz defaults to x86_64-unknown-linux-musl on Linux so ASAN can
statically link, but dtolnay/rust-toolchain@nightly doesn't ship a
musl std and GH runners don't have it preinstalled — build failed with
'can't find crate for core' on byteorder. Pinning both fuzz build and
fuzz run to the gnu target uses the std that comes with the toolchain
and works out of the box.
2026-04-13 14:07:24 -06:00
swaits a21e368e18 chore: prepare 0.1.0 for crates.io publish
- Cargo.toml: add documentation and readme fields; expand the exclude
  list so deny.toml, justfile, mise.toml, .github, .jj, fuzz, and any
  cargo-mutants output never ship in the published tarball.
- README: badges, install snippet for both feature shapes, testing
  matrix.
- CHANGELOG.md: keep-a-changelog skeleton with a 0.1.0 entry.
- Revert framing.rs let-chain back to nested if-let so the published
  MSRV holds at the edition-2024 floor (1.85). Behaviour unchanged.

cargo publish --dry-run packages cleanly.
2026-04-13 12:12:12 -06:00
swaits 670321efd1 ci: GitHub Actions workflow + drop test-fuzz-long
Eight parallel jobs on push/PR:

- test × 3 (default / no-default / all-features) via nextest + doc
- clippy × 3 feature permutations, -D warnings
- fmt
- doc (all-features, -D warnings on rustdoc)
- deny (cargo-deny action)
- miri (nightly, lib + vectors under default + no-default)
- fuzz (nightly, 60 s smoke on every target)
- mutants × 2 feature configs (same equivalence exclusions as local)

Swatinem/rust-cache on every cargo job; taiki-e/install-action for
nextest / cargo-fuzz / cargo-mutants. Uploads mutants.out on failure.

Drops test-fuzz-long from the justfile — 60 s per target in CI every
push plus the local proptest 1M-case suite already covers this.
2026-04-13 12:12:12 -06:00
swaits 78cb84d8da test: run mutants under both feature configurations
test-mutants now runs cargo-mutants twice — once with defaults (defmt
derives present) and once with --no-default-features (derives absent)
— so feature-gated paths are covered in both shapes. Same three
equivalent mutants are excluded in both runs.
2026-04-13 12:12:12 -06:00
swaits ef576ba1ec test: three more cargo-fuzz targets + test-fuzz-long recipe
- command_roundtrip: structure-aware (via arbitrary crate) Command
  encode → parse → equal. Symmetric to cobs_roundtrip.
- cobs_decoder_multi_frame: chunked feeding with arbitrary split sizes;
  asserts bounded payload and that command count never exceeds sentinel
  count.
- cobs_chunk_invariant: same stream through two decoders (chunked vs
  all-at-once) must produce the same command sequence.

Adds test-fuzz-long recipe (600s per target, ~1h 10m total) for CI
cron / pre-release. All seven targets smoke-tested at 100k runs each
with zero findings.
2026-04-13 12:12:12 -06:00
swaits adbad81e19 test: 23 proptests across two budget tiers (1M+ cases per run)
Scaled the proptest suite hard and added 13 new properties:

- cobs_decoder_multi_frame: N frames decode to N commands in order.
- cobs_decoder_chunk_invariant: arbitrary chunk splits yield the same
  sequence as feed-all-at-once.
- cobs_decoder_byte_by_byte_equivalent: stronger form of chunking.
- cobs_decoder_recovers_after_garbage: decoder is not poisoned by a
  leading garbage frame.
- cobs_decoder_ignores_sentinel_runs: arbitrary leading/trailing 0x00
  runs do not affect decoding.
- cobs_decoder_oversized_discards: frames > MAX_FRAME are discarded;
  subsequent frames still decode.
- response_full_roundtrip: encode + COBS + decode + re-parse yields the
  original Response (via a test-only decode_response helper).
- response_encoding_deterministic: byte-identical output across runs.
- cobs_encoded_has_single_trailing_zero: COBS invariant check.
- bandwidth_u8_is_bijection: sweeps the whole u8 space.
- radio_config_write_to_stays_in_bounds: buffer-write sanity.
- resolve_then_validate_consistent: cross-field contract.

Split into two proptest! blocks: lightweight at 100,000 cases each, and
heavyweight (MAX_FRAME-sized buffers, multi-frame streams) at 10,000.
Total ~1.13M test cases per suite run, finishing in ~20 s.
2026-04-13 12:12:12 -06:00
swaits 7a9820e495 test: miri-clean + mutation-hardened
Adds tests/mutation_killers.rs with six targeted tests derived from
cargo-mutants findings: cad_enabled true/false paths, Transmit
with-config boundary, CobsDecoder::reset behaviour, empty-frame
handling, and the cobs_encode_response success path.

Three mutants remain as provably equivalent — the justfile excludes
them by exact regex so any genuinely new mutant on the same line will
still surface. Updates test-miri to also run the integration tests
(proptest stays skipped because its file-persistence layer calls
getcwd). Gitignore picks up mutants.out / mutants.out.old.

cargo mutants: 89 caught, 9 unviable, 0 missed.
2026-04-13 12:12:12 -06:00
swaits 6acc12cde8 test: canonical wire-byte vectors anchored to PROTOCOL.md
Hand-encoded byte sequences for RadioConfig, every Command tag, and
every Response tag, taken directly from the spec. Fields use hex-clean
values (freq_hz = 0x12345678, sync_word = 0xABCD, etc.) so each
little-endian byte is visually verifiable.

Locks firmware and host to the written spec — a refactor that quietly
changes a field layout fails here, not in the field.
2026-04-13 12:12:12 -06:00
swaits 53f801ec0b chore: add full-treatment quality gates
Mirrors the ucobs convention:
- justfile aggregates test-{unit,clippy,fmt,doc,deny,fuzz,miri,mutants}
  behind a single 'just test' target.
- mise.toml pins rust + cargo-{deny,fuzz,mutants,nextest}.
- deny.toml enforces permissive licenses only and bans wildcard /
  unknown-source deps.
- CLAUDE.md captures identity and first principles.
- Cargo.toml adds unsafe_code = forbid and docs.rs all-features metadata.

cargo check (all feature permutations), nextest, clippy with warnings
as errors, fmt check, doc, and deny all pass. fuzz/miri/mutants land
in follow-up commits.
2026-04-13 12:12:12 -06:00
swaits 0d0dc81879 test: cargo-fuzz targets on parser and framing
Four libFuzzer targets:
- parse_command: Command::from_bytes never panics; bounded payload.
- parse_radio_config: RadioConfig::from_bytes never panics.
- cobs_decoder_feed: CobsDecoder::feed is panic-free across arbitrary
  byte streams split into arbitrary chunks.
- cobs_roundtrip: structure-aware (via arbitrary crate) Response →
  cobs_encode_response → ucobs::decode yields identical raw bytes.

All four smoke-tested at 50k runs each with no findings. Intended CI
cadence is a weekly cron with a larger time budget; per-PR would be
expensive. Requires nightly + cargo-fuzz.
2026-04-13 09:05:07 -06:00
swaits 79587f9e22 test: proptest suite for protocol and COBS framing
Ten properties covering: RadioConfig roundtrip and validate/resolve
determinism; no-panic on arbitrary bytes for both Command::from_bytes
and RadioConfig::from_bytes; bounded-capacity for Transmit payloads
(guards the heapless::Vec size); Command roundtrip via a test-only
encoder (the library only decodes commands); COBS reversibility; and
two streaming CobsDecoder properties (no-panic on random byte streams
with random chunk boundaries, and a full encode→feed→decode roundtrip).

Adds Clone to Command and Response — benign public-API expansion that
unblocks proptest's Just strategy.
2026-04-13 09:03:40 -06:00
swaits 548cf722c2 feat: add COBS framing module
Imports CobsDecoder and cobs_encode_response from firmware's
src/host/framing.rs, adjusted so the defmt::warn on encode overflow is
gated on the crate's defmt feature rather than an unconditional import.

The route_command helper stays in firmware — it depends on embassy
channels, which have no business in a transport-agnostic crate.
2026-04-13 09:01:11 -06:00
swaits a9ab81a27f feat: import wire protocol types from firmware
Copied src/protocol.rs from firmware verbatim, added crate-level #![no_std],
migrated the defmt::Format derive gate from cfg_attr(not(test), ...) to
cfg_attr(feature = "defmt", ...) so host-side (no-default-features) builds
compile cleanly. Tests unchanged; all 35 pass.

Firmware still owns the file for now — the re-export swap lands in a
follow-up firmware commit to keep each change standalone.
2026-04-13 09:00:34 -06:00