Skip to content

AX.25: XID/MDL responder + peer-capability cache — activates SREJ + v2.2-preferred connect - #63

Merged
M0LTE merged 7 commits into
mainfrom
feat/xid-mdl-preconnect
Jul 12, 2026
Merged

AX.25: XID/MDL responder + peer-capability cache — activates SREJ + v2.2-preferred connect#63
M0LTE merged 7 commits into
mainfrom
feat/xid-mdl-preconnect

Conversation

@M0LTE

@M0LTE M0LTE commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

The XID keystone (Table A #4/#5, In). Byte-for-byte with C# Packet.Ax25.Xid (validated against spec Figure 4.5-4.6 golden bytes + XidInfoFieldTests):

  • XID info-field TLV codec (new ax25/xid/*) — FI/GI/GL + PI/PL/PV, MSB-first PV, ascending-PI, XidParseOptions (Strict default).
  • Wire classify — XID/FRMR/TEST now decode (were ControlFieldError); FrameSpec::Xid outbound.
  • Pre-session XID responder (new sdl/mdl.rs) — §6.3.2 reverts-to merge, answers inbound XID (F=1, no ConnectIndication), stages negotiated params so the following SABM preserves them. Mirrors RespondToXidCommand/HandleNoCachedSession.
  • Peer-capability cache (new sdl/capability.rs) — fixed-capacity port of PeerCapabilityCache; connect_planned dial seam.
  • Activates SREJ — negotiated-SREJ link emits SREJ vs REJ on go-back-N (previously dormant — srej_enabled was never set).
  • prefer_extended_connect default → true (matches C# PreferExtendedConnect), with a DM-refusing-peer degrade-to-mod-8 regression (safe now fw: unified node web panel (pre-filled config + firmware + maintenance) #48 is on main).
  • XID golden vectorsvectors/xid.json + runner; drift-guard passes 0 gaps.

+69 tests (702 total). clippy/no_std/fw-build/drift-guard all green.

One flagged residual: the initiator pre-connect XID probe (NegotiateSrejBeforeConnectAsync — proactively sending our XID before we dial) is NOT included — an async multi-step flow best driven fw-side; the agent stopped rather than guess. The responder half is complete and the seam (apply_negotiated + capability.rs) is in place for it.

🤖 Generated with Claude Code

Tom Fanning and others added 7 commits July 12, 2026 19:46
…te with C#)

New `src/ax25/xid/` module mirroring `Packet.Ax25.Xid`:

- `info_field` — FI/GI/GL header + ordered PI/PL/PV parameter codec
  (`encode`/`encode_into`/`parse`/`parse_with`). Zero-alloc `encode_into`;
  strict-by-construction encode; ascending-PI ordering; §3.8 MSB-octet-first
  HDLC PV.
- `parameters::XidParameters` — decoded parameter set (`None` = absent).
- `classes_of_procedures::ClassesOfProcedures` — PI=2 duplex (ABM bit 0).
- `hdlc_optional_functions` — PI=3 reject/modulo/segmenter + `RejectMode`.
- `parse_options::XidParseOptions` — Strict (default) / Lenient leniency knobs.

Tests lift the exact Figure 4.6 worked-example bytes and the strict-vs-lenient
pairs from the C# `XidInfoFieldTests`. 27 XID tests green; clippy clean;
no_std (--no-default-features --features alloc) builds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
- bridge `classify_incoming`: 0xAF/0x87/0xE3 now map to XidReceived /
  FrmrReceived / TestReceived instead of falling to ControlFieldError. XID is a
  single XidReceived event (the command/response split rides in
  FrameInfo::is_command), matching the C# Ax25FrameClassifier.
- signal: new FrameSpec::Xid { is_command, pf, info } — an XID U-frame carrying
  no PID, info = encoded XID parameter TLVs. build_frame gains its arm; it falls
  through encode_spec's extended match (U-frames are 1 octet in both modulos).

Tests: XID command/response classify, FRMR/TEST classify, and a
FrameSpec::Xid build→decode→classify round-trip. 664 core tests green; clippy
clean; no_std builds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…stener)

New `src/sdl/mdl.rs` porting the on-air MDL pieces:

- `apply_negotiated` — the §6.3.2 reverts-to merge (lesser-of reject/modulo,
  min-of window/N1, max-of T1/N2, mutual-AND segmenter), mirroring
  `XidNegotiator.ApplyNegotiated`.
- `apply_version_20_defaults` — the §1436 full v2.0 default set.
- `default_offer_for` — derive our XID offer from the session context.
- `respond_to_xid_command` / `respond_pre_session_xid` — the un-transcribed
  figc5.1 responder: seed SREJ-capable, merge the command's offer, apply agreed
  values to the context, return the F=1 XID response info. Mirrors
  `Ax25ManagementDataLink.RespondToXidCommand` + `HandleNoCachedSession`.

Manager wiring: `post_with_local` intercepts an inbound XID *command* for a
Disconnected (pre-session) slot with accept_incoming, answers it directly
(connectionless — no LM-SEIZE, no ConnectIndication), and stages the negotiated
params on the cached slot so the subsequent SABM's Set Version 2.0 preserves
`srej_enabled` into the link.

Tests: the §6.3.2 merge per-parameter (mirroring XidNegotiatorTests), the v2.0
defaults, the responder (SREJ kept/reverted/empty-info), and two manager
integration tests mirroring Ax25ListenerPreSessionXidTests (XID command →
XID response, not DM; SABM-after-XID → Connected with SREJ adopted).

677 core tests green; clippy clean; no_std builds; fw release build + test
cross-compile green.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…eerCapabilityCache)

New `src/sdl/capability.rs` — a fixed-capacity, no_std, alloc-free port of
`Packet.Node.Core.Capabilities.PeerCapabilityCache`:

- `plan_dial(port, peer, policy, now_ms)` — miss ⇒ optimistic policy default;
  fresh learned positive/negative honoured; stale (>30d) re-probed. Extended and
  pre-connect-XID decisions exactly as C# PlanDial.
- `record_outcome(...)` — plan-aware learning (a dimension is learned only when
  the dial probed it; last_refused stamped on an extended degrade, carried
  forward otherwise), mirroring RecordOutcome.
- `PeerCapabilityRecord` / `PeerDialPlan` / `PeerDialPolicy`.

Embedded deviations (documented): in-memory only (no SQLite; LRU-evict on full),
u8 port index instead of a string PortId, explicit now_ms u64 instead of
TimeProvider.

Manager: `connect_planned(local, peer, plan, timers)` supplies the plan's
extended choice at dial time (SABME vs SABM). The initiator pre-connect-XID
*probe* (async NegotiateSrejBeforeConnectAsync fast-probe) is documented as
deferred — the responder half (task 3) + apply_negotiated are in place for a
fw-side initiator MDL.

26 capability tests (mirroring PeerCapabilityCacheTests) + a connect_planned
test. 698 core tests green; clippy clean; no_std builds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
The SREJ recovery code was correct but dormant: `srej_enabled` was never set
(no XID machine). Task 3's pre-session responder / `apply_negotiated` now sets it
from the negotiated XID, so Selective Reject engages on-air.

Tests: a negotiated-SREJ link emits an SREJ (targeting the gap V(R)) on an
out-of-sequence I-frame and stores the frame; a go-back-N link (SREJ not
negotiated) emits REJ instead — proving the SREJ path is gated on the
XID-negotiated `srej_enabled`. Added a Recorder `supervisory()` helper.

Clippy clean.

Co-Authored-By: Claude Code <noreply@anthropic.com>
…endedConnect)

Now that both refusal degrades are present on the session (FRMR fallback #45 and
DM-refusal degrade #48), a plain `SessionManager::connect` defaults to a mod-128
SABME-first dial — matching `Ax25ListenerOptions.PreferExtendedConnect = true` —
and degrades to a mod-8 SABM re-establishment if the peer refuses, rather than
stranding the connect.

- `SessionManager::new`: prefer_extended_connect true; updated docs on `new` and
  `with_prefer_extended_connect` (removed the stale "#48 owned by another track"
  rationale).
- Updated `plain_connect_honours_prefer_extended_default` for the new default
  (SABME-first; opt-out → SABM).
- New regression `default_extended_connect_degrades_to_mod8_sabm_on_dm_refusal`:
  a DM-refusing peer degrades to mod-8 SABM (the DM analogue of the FRMR test) —
  the safety net that makes the flipped default safe.

701 core tests green; clippy clean; no_std builds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
`vectors/xid.json` carries C#-derived XID information-field bytes (the exact
Figure 4.6 worked-example bytes XidInfoFieldTests pins, canonical + literal-print
variants, the empty header, and a mod-8 SREJ interlink offer). New
`tests/xid_golden_vectors.rs` parses each vector, asserts the decoded parameters,
and (when roundtrip) re-encodes byte-identical — proving the pico-node XID codec
is byte-for-byte with Packet.Ax25.Xid.

The parity drift-guard (`node scripts/parity-check.mjs`) passes (0 gaps): the
xid vector set was already opted-in; this backs the declaration with real
byte-parity vectors.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@M0LTE
M0LTE merged commit 407aa0a into main Jul 12, 2026
M0LTE added a commit that referenced this pull request Jul 12, 2026
docs(roadmap): reflect the XID/MDL + SDM-link wave (#62/#63)
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.

1 participant