O5 pod simulator support #17
Open
jwoglom wants to merge 18 commits into
Open
Conversation
Add Mode enum (Dash, O5) and a -mode CLI flag (default "dash" so existing users see no behavior change). Plumb the selected mode through Pod into the Pair instance constructed in StartActivation. Add the Omnipod 5 KDF: SHA-256 over a length-prefixed buffer of FIRMWARE_ID || 0 || pdmPublic || podPublic || sharedSecret, split into 16-byte conf and 16-byte ltk. In computePairData, when Mode == O5, derive (conf, ltk) via the new KDF and skip the Dash CMAC chain entirely. The KDF expects 64-byte raw P-256 public keys; real P-256 key plumbing arrives with the SPS2 work in a later commit. Tests in o5kdf_test.go exercise the KDF in isolation with synthetic 64-byte fixtures. Source: jwoglom/five commit 6e2fa66 (Step 1).
Embed BTSNOOP captures from real Omnipod 5 pairing sessions as Go test fixtures. The new pkg/testfixtures package exposes Captures(), returning three deterministic PairingCapture values with SPS1/SPS2.1/SPS2 byte strings from two different pod sessions. These will be consumed by SPS2 and Type-4 verification tests in later commits. Bump go.mod 1.15 -> 1.20 (go:embed requires 1.16+; 1.20 matches the rest of the porting work). go.sum already had the needed entries from prior vendoring; vendor/modules.txt picks up the explicit marker for the two indirect modules that Go 1.17+ requires to be declared. Source: jwoglom/five commit 9551b13 (Step 0).
Add pkg/bluetooth/packet — a platform-neutral implementation of the OmnipodKit BLE packet split/join wire format. Split() emits properly framed fragments (single-packet, first/middle/last/optional+1) and Join() reassembles them using each fragment's actual length, so smaller negotiated MTUs (down to ~30 bytes) work without code changes. Tests cover the round-trip, CRC tamper detection, index mismatch rejection, the exact-fit boundary, and shorter fragments. Fix the TWi length-decode bug in pkg/message: `data[6]<<3 | data[7]>>5` was a uint8 operation, silently truncating any payload longer than 255 bytes (an SPS2.1 frame at 651 bytes decoded as 139). Cast both halves to uint16 before shifting; update the three slice expressions to take int(n) so the type widens cleanly. New length_test.go pins this at SPS2.1 size and at the 11-bit field maximum (2047 bytes). Add MessageTypeEncryptedSigned = 4 constant so later commits don't re-touch this file just to declare it. Full Type-4 Marshal/Unmarshal plumbing arrives in a later commit. bluetooth.go still uses its existing I/O paths; the swap to packet.Split /Join happens in the next commit. Source: jwoglom/five commits a472229, a9bdabd, 0be4858 (TWi fix).
Layer the Omnipod 5 BLE shape onto current main's bluetooth.go without regressing main's transport fixes from 79be48c. Three things change: 1. Advertising / GATT services (from five 9ea07e3): - Co-advertise ECF301E2-... alongside CE1F923D-... so OmnipodKit's scanner discovers the simulator. - Heartbeat service 7DED7A6C-... with notify characteristic 7DED7A6D-..., backed by a 10-second keep-alive goroutine that fires a one-byte notification once a central subscribes. - GATT data characteristic UUID corrected to canonical OmnipodKit form (1A7E2443-...). 2. Packet split/join I/O (from five a472229): the loop now dispatches to three channels — outgoing messages go through writeMessageData (using packet.Split), inbound data fragments go through readMessageData (using packet.Join driven by a closure that pulls subsequent fragments from b.ReadData()), and the legacy cmdInput path is retained for the existing fragmentation handshake. 3. cmdActivation channel scaffolding: the channel is declared and allocated, but ReadCmd still reads from cmdInput exactly like main. The HELLO-vs-RTS routing fix (76fd556) that feeds cmdActivation arrives in Commit 9; the channel exists now so that commit doesn't have to touch the struct definition. What was deliberately preserved from main (79be48c et al.): - writeMessage's integer arithmetic for large status responses (fullFragments / rest / index as int with byte() conversions) — five silently regressed this back to byte arithmetic. - StopMessageLoop call in CentralConnected (main's 97ce95d revert; five branched before that). Deviation forced by environment: AdvertiseNameServicesMfgData is not present in the vendored paypal/gatt on this branch (five carries a vendor patch we are not pulling in). Falls back to AdvertiseNameAndServices in both initial advertise and refresh paths; the manufacturing-data payload is therefore not advertised. Source: jwoglom/five commits 9ea07e3 (advertising/heartbeat), a472229 (packet split/join), plus partial 76fd556 (channel scaffolding only).
Add the SPS2.1 + SPS2 cryptographic exchange used by Omnipod 5 pairing, plus the synthetic pod-side PKI that backs it. New files in pkg/pair: - podidentity.go: P-256 keypair + self-signed X.509 cert. NewPodIdentity generates fresh material; LoadPodIdentity rehydrates from persisted raw scalar + DER. Each simulator instance gets a stable identity on first activation. - spsnonce.go: 13-byte AES-CCM nonce builder with separate read/write counters keyed off the SPS exchange's nonce halves. - sps2.go: AES-CCM encrypt/decrypt for SPS2.1 (cert) and SPS2 (cert||sig), 171-byte channel-binding transcript builders for both PDM and pod directions, SHA-256+ECDSA sign/verify with raw 64-byte r||s pack/unpack, and a P-256 SPKI extractor that reads the public key out of a DER cert without doing chain validation. Also includes VerifyType4Signature, which lands here pre-wired but is unused until the Type-4 framing commit. - sps2_test.go, spsnonce_test.go: round-trips, transcript binding, P-256 extraction. pair.go hand-merge (preserve Dash bit-for-bit): - Pair struct gains nonceState, identity, pdmCertDER, sps21Data. - SetIdentity / IsO5 accessors. - computeMyData(): Dash unchanged (32-byte Curve25519 + zeroed nonce). O5 mints a real P-256 keypair via crypto/ecdh and a 16-byte random nonce. - computePairData(): Dash CMAC chain unchanged. O5 does P-256 ECDH (0x04||pdmPublic), feeds o5DeriveKeys with the now-matching 64/64/32 inputs, and initialises nonceState. - ParseSPS1 branches on Mode for the wire public-key length (32 vs 64). - ParseSPS2 / GenerateSPS2: O5 path decrypts/encrypts cert||sig, builds transcripts using pre-decrypt nonce state, signs with the identity key, and soft-fails PDM signature verification (warns but doesn't fatal — transcript layouts can shift across firmware). - ParseSPS21 / GenerateSPS21: new entry points; O5 decrypts/encrypts the intermediate-CA cert. Dash returns a zero-fill stand-in. pod.go: - ensurePodIdentity loads from state if persisted, else mints and saves. - StartActivation renames the local pair value to pairCtx (so calls like pairCtx.IsO5() and pairCtx.SetIdentity() don't fight the package identifier), calls SetIdentity for O5, and inserts the SPS2.1 send/receive between SPS1 and SPS2 — gated on IsO5(), so the Dash pairing sequence is unchanged. state.go: adds O5PrivateKey + O5CertDER (TOML keys o5_private_key / o5_cert_der) so the pod identity survives across runs. Source: jwoglom/five commit b459d39 (Step 3). The copy of sps2.go reflects five's HEAD (includes VerifyType4Signature), which the next commit will wire up.
Add the pkg/aid package: parses and responds to the nine Algorithm Integration Device (AID) Phase-1 setup commands that fire between AssignAddress and SetupPod during O5 pairing (UTC time, TDI, target BG profile, DIA, EGV, three batches of insulin history, status queries). AID commands are plain-ASCII payloads carried inside the same AES-CCM Type-1 encrypted transport as standard commands — they are NOT SLPE wrapped, so they require their own dispatcher. The aid.IsAIDPayload function distinguishes them by checking for a non-"0" feature prefix; aid.Parse decodes them length-anchored so the trailing ",G<f>.<a>" suffix on SET+GET commands works binary-safely; cmd.BuildResponse emits canned Gen1 Status (28B), Unified Status (29B), echoes data on SET+GET, and returns "0" ack for Extended SET. pod.CommandLoop gets a routing branch after decryption: when aid.IsAIDPayload matches, hand off to handleAIDCommand and continue — the standard command.Unmarshal path is untouched. The branch is gated purely on payload shape, so Dash sessions (which never send AID commands) flow through exactly as before. handleAIDCommand encrypts the response via the existing encrypt.EncryptMessage path, sends it, and reads the controller's empty-payload ACK to keep nonces in sync, mirroring the standard post-response flow at the bottom of CommandLoop. Note: handleAIDCommand deliberately does NOT call notifyStateChange() while holding p.mtx — the CommandLoop's existing post-unlock call handles that. This matches the post-acbfbb2 layout (the deadlock fix that lands later in a runtime-fixes commit). e4bd7c3 already shipped the fixed form, so it's ported verbatim. Source: jwoglom/five commit e4bd7c3 (Step 4).
Add the Type-4 (MessageTypeEncryptedSigned) wire format used by the
Omnipod 5 controller for post-pairing commands. Layout:
[16-byte TWi header][ciphertext (plaintext-len + 8-byte CCM tag)]
[64-byte ECDSA r||s signature]
The header's 11-bit length field reflects only the ciphertext, NOT the
trailing signature.
pkg/message/message.go:
- Add Signature []byte to the Message struct.
- Marshal: extend the EncryptedPayload fast-path that returns m.Raw to
also cover Type-4 (m.Raw already includes the appended signature).
- Unmarshal: new Type-4 branch slices ciphertext as data[16:16+n+8] and
signature as the next 64 bytes; errors on truncation.
- Widen the type-range guard to allow MessageTypeEncryptedSigned.
- Fix flag.set: the old form was a no-op when val=false, leaving leftover
bits set on reused *flag values. Without this, the type-4 bit pattern
could mangle into type-5 when a writer reused a flag buffer. Now clears
bits via *f &^= mask.
pkg/pair/pair.go: cache the PDM's 64-byte raw P-256 public key (X||Y,
left-zero-padded) at ParseSPS2 time by running extractP256PublicKey on
the PDM cert. PDMPublicKey() exposes it.
pkg/pod/state.go: PDMPublicKey []byte (TOML pdm_public_key) so the
cached key survives across simulator runs.
pkg/pod/pod.go:
- StartActivation: after LTK is set, copy pairCtx.PDMPublicKey() into
state and save.
- CommandLoop: before decryption, if msg.Type == MessageTypeEncryptedSigned,
call pair.VerifyType4Signature(pdmKey, msg.Raw[:16], msg.Payload,
msg.Signature). Soft-fail: warn on missing pubkey / malformed sig /
verify error / failed verification; log success otherwise. Never fatal,
always continue with decryption — transcript layouts can drift across
firmware revisions.
Tests: TestUnmarshalType4 + TestUnmarshalType4Truncated for the wire
layout, TestVerifyType4Signature_RoundTrip + bad-input rejection for
the verification helper.
Source: jwoglom/five commit 34fc6ad (Step 5).
Land the plumbing for mode-aware SetUniqueID (0x03) and GetVersion (0x07)
responses without changing any byte output today. When the real Omnipod 5
byte captures arrive from Joe, swapping the O5 constants will be a
one-line change with the regression tests already in place to catch any
accidental Dash drift.
Plumbing:
- pkg/pod/state.go: persist Mode (pair.Mode) in PODState (TOML key mode).
Pod.New writes state.Mode = pairMode on every launch so the -mode flag
is the source of truth for the current run.
- pkg/command/command.go: optional ResponseForMode interface that any
Command can implement to return mode-specific bytes. Existing Command
interface is unchanged so the change ripples nowhere else.
- pkg/pod/pod.go CommandLoop: when cmd.IsResponseHardcoded(), type-assert
for ResponseForMode and prefer it; fall through to GetResponse() for
every other hardcoded command.
- pkg/command/{setuniqueid,getversion}.go: implement GetResponseForMode,
threading the Mode through to the response struct.
- pkg/response/{setuniqueidresponse,versionresponse}.go: add Mode field,
split the hex constant into named dash*ResponseHex + o5*ResponseHex
(today identical, TODO(joe) comment marks the swap point). Marshal
selects on Mode; zero value (ModeDash) preserves legacy behaviour for
any caller still constructing the struct with bare {}.
Tests pin both Dash hex strings against the captured byte sequences on
current main and verify the zero-value path is Dash, so any future change
to the legacy constants must consciously update the regression. O5 tests
mirror the Dash bytes today with parallel TODO(joe) markers so the
assertion will be updated alongside the real-bytes swap.
Dash bit-for-bit unchanged.
Bundle four small upstream runtime fixes needed for O5 sessions to survive past pairing. bluetooth.go (76fd556 — HELLO routing): - The CMD-char write handler now classifies every write: multi-byte payloads (e.g. the OmnipodKit HELLO frame 06 01 04 + 4-byte ID) and non-RTS single-byte signals go to cmdActivation; the four fragmentation-control signals (RTS / SUCCESS / NACK / FAIL) stay on cmdInput. - ReadCmd reads from cmdActivation again, completing the dispatcher wiring that Commit 3b scaffolded. - The legacy RTS path's first-byte check no longer fatals on an unexpected value — it warns and returns nil. Misclassified future signal bytes can't kill the loop. bluetooth.go (b674a06 — clean shutdown): - ShutdownConnection now calls StopMessageLoop() so the idle-timeout reconnect can re-init the pipeline without the "Messaging loop is already running" fatal. pod.go (acbfbb2 — AID deadlock): - The AID branch in CommandLoop now calls notifyStateChange() AFTER p.mtx.Unlock(). handleAIDCommand itself still must not call it while holding the mutex (documented). Without this notification, web clients watching state wouldn't see AID-phase progress at all. pkg/pod/delivery (4ce6046 — pulse-by-pulse math): - New package with PartialPulses(start, end, totalPulses, now) — pure interpolation math that's testable on macOS without the gatt dependency. delivery_test.go covers 2-second/pulse user boluses, 1-second/pulse setup boluses (prime/cannula), and the degenerate boundary cases. - Not wired into the live bolus accounting today: main's existing immediate-decrement model (BolusRemaining + Delivered) is preserved bit-for-bit. The package is available as a supplemental helper for future work (e.g. WS API exposure of pulse-level delivery progress). Prime pulse cadence (b674a06 in pod.go): already implemented by main's existing `if PodProgress >= PodProgressRunningAbove50U` branch selecting 1s/pulse during setup vs 2s/pulse during user boluses. No edit was needed — the b674a06 snapshot pattern expresses the same rule in the snapshot-bolus model that we didn't adopt. Source: jwoglom/five commits 76fd556, acbfbb2, b674a06, 4ce6046.
Replace the placeholder o5*ResponseHex constants (which mirrored Dash) with the actual byte streams captured from a real Omnipod 5 pod (Pod Type ID 05, firmware 9.0.4, BLE firmware 5.0.2, Lot 261724721, TID 491153). Both files now carry the upstream byte-field layout comment so the structure of each frame is self-documenting. SetUniqueID (0x011B response to 0x03): 01 1B 1388 10 08 34 0A 50 09 00 04 05 00 02 05 03 0F999A31 00077E91 00000000 = 01 LL VVVV BR PR PP CP PL MXMYMZ IXIYIZ ID 0J LLLLLLLL TTTTTTTT IIIIIIII VersionResponse (0x0115 response to 0x07): 01 15 09 00 04 05 00 02 05 02 0F999A31 00077E91 05 FFFFFFFF = 01 LL MXMYMZ IXIYIZ ID 0J LLLLLLLL TTTTTTTT GS IIIIIIII The Dash regression tests are unchanged and still pin the legacy bytes verbatim; the O5 tests now assert the real captured hex. Mode-aware plumbing (the optional ResponseForMode interface, the Mode field on PODState, the type-assertion dispatch in CommandLoop) all landed in the previous response commit — this is purely the constant + test swap that commit was scaffolded for.
Test❌ failed to pair Code ReviewThe code review is beyond my knowledge. I simply did a test ConfigurationTest phone running Loop-TP v3.15.0 (tidepool-merge-add_omnipodkit)
Test NarrativerPi 3bUse rPi 3b as the simulator. delete the pod folder and copy over fresh from Mac the branch: claude/o5-port-synthesis-eCiOB Issue commands on rPi: then Attempt to pair DASH and get No pods found. Try all my tricks (reboot, rebuild, etc) and no joy. For dash tried: For O5 tried: In case this is an rPi 3b problem, try this on my rPi 4. rPi 4go version go1.22.3 linux/arm64 bring over claude/o5-port-synthesis-eCiOB still No pods found. Back to you @jwoglom |
PR review revealed that every BLE-related change in Commit 3b was applied unconditionally to both Dash and Omnipod 5 sessions because the selected pair.Mode never reached pkg/bluetooth. As a result Dash users (default mode) saw the simulator advertise as an O5 pod, expose the wrong data characteristic UUID, and frame messages using O5's 244-byte packet split instead of Dash's 20-byte RTS/CTS transport. Marion's Raspberry Pi sees "No pods found" for both modes as a consequence. This commit is pure infrastructure: thread the mode through New so subsequent commits can branch advertise / GATT / transport / cmd routing on it. No wire bytes change yet; the simulator behaves exactly as it did before, just with the mode stored on the Ble struct and an IsO5() accessor available. - pkg/bluetooth/bluetooth.go: add mode pair.Mode field on Ble, accept it as the third parameter to New, log the active profile at startup, add IsO5() helper used by the follow-up mode-branched code. - main.go: pass pairMode through to bluetooth.New. This is the first of a focused repair series (A-H) that restores Dash bit-for-bit against origin/main while keeping the Omnipod 5 work intact.
The Omnipod 5 port replaced the Dash advertise call with the O5 form globally — same code path for both modes. Result: a Dash phone scanning the simulator saw an "AP <hex> 0A95B6110002761B" device announcing CE1F923D... and ECF301E2... UUIDs instead of " :: Fake POD ::" and the 9-element 16-bit UUID list it expects, so the Dash app reported "No pods found". Split the advertise paths into four mode-specific helpers and branch on b.IsO5() at each site: - advertiseDash / refreshDash carry main's exact bytes: name " :: Fake POD ::", UUIDs 0x4024, 0x2470, 0x000a, the two pod-address UUID16 slots, and the version-response identifiers 0x0814, 0x6DB1, 0x0006, 0xE451. refreshDash preserves main's tracing form verbatim (including the two log.Tracef calls that vet would flag — they are inherited from main and changing them would break byte parity). - advertiseO5 / refreshO5 keep the current HEAD behavior unchanged: "AP <hex> 0A95B6110002761B" name, CE1F923D-...0A<podId>00 and ECF301E2-... UUIDs. The manufacturer-data field (60030001000000) that real O5 pods co-advertise is still missing in this commit; Commit F will land that once the vendored paypal/gatt gains the helper it needs. onStateChanged and RefreshAdvertisingWithSpecifiedId both dispatch on b.IsO5(). The startup log now reports the actual advertised name and UUID count so a btmon capture is straightforward to correlate. GATT shape (data char UUID, heartbeat service, registration form), transport framing, and command-channel routing are still O5-shaped for both modes — they are addressed in Commits C, D, and E.
The Omnipod 5 port changed the data-characteristic UUID from
1A7E2442-... (Dash) to 1A7E2443-... (O5) globally, and registered an O5
heartbeat service unconditionally via d.SetServices. Dash clients
subscribe to and write on 2442 — when the simulator only exposed 2443,
Dash discovery succeeded but every subsequent data exchange failed
silently. Combined with the advertise regression fixed in Commit B, this
was the second half of Marion's "No pods found / pods found but won't
pair" symptoms.
Mode-branch the GATT registration inside onStateChanged:
- Dash: single service with cmd char 1A7E2441-..., data char
1A7E2442-..., registered via d.AddService(s) — exact origin/main
shape, no heartbeat service.
- O5: same service + cmd char, data char 1A7E2443-..., heartbeat
service 7DED7A6C-... with notify characteristic 7DED7A6D-...,
registered via d.SetServices([]*gatt.Service{s, h}) — current HEAD
behavior preserved.
The cmd-characteristic Write/Notify callbacks and the data-char
Notify/Write callbacks are unchanged for both modes (Commit E will
mode-branch the cmd dispatcher; the data callback is identical because
the wire bytes differ in framing, not in queueing).
The heartbeat goroutine still spawns unconditionally in New, but its
existing nil-check on b.heartbeatNotifier makes it a no-op for Dash
since the heartbeat service is never registered and the notifier stays
nil. Documented inline so a future reader can confirm the invariant.
The Omnipod 5 port routed all message I/O through packet.Split/Join (244-byte fragments) unconditionally, so Dash phones — which frame at 20 bytes via the RTS/CTS/SUCCESS handshake — either rejected the oversized outbound fragments or failed to reassemble inbound responses sized differently from what packet.Join expected. The legacy writeMessage and readMessage paths still existed in the file but were no longer invoked by loop(). Split the message loop into two mode-specific siblings and dispatch on b.IsO5() once at entry: - loopDash: messageOutput -> writeMessage (legacy hand-rolled 20-byte fragments with RTS/CTS/SUCCESS handshake), cmdInput -> readMessage (consumes data fragments inline via b.ReadData() during the handshake). No data-input case — Dash never reads data outside of readMessage's RTS path, so a select case on dataInput would race readMessage's own reads. - loopO5: messageOutput -> writeMessageData (packet.Split, up to 244B per fragment), dataInput -> readMessageData -> parsePackets (packet.Join with closure-driven fragment fetch), and cmdInput -> readMessage retained as a harmless fallback (warns and returns nil for non-RTS bytes — O5 phones are not expected to send RTS at all). The packet subpackage's MaxPayloadSize = 244 stays; only the O5 path reaches it. message.go (TWi length fix, Type-4) and the cmd-char dispatcher are unchanged in this commit — Commit E will mode-branch the cmd routing. Also pulled in main's c797a81 fix to the two refreshDash Tracef calls (they need %v format directives or go test -vet fails CI).
The Omnipod 5 port introduced a dispatcher on the CMD characteristic that splits multi-byte / non-control writes onto a new cmdActivation channel, and pointed ReadCmd at cmdActivation. The intent was correct for O5 (HELLO arrives as a 7-byte frame and must reach pod.StartAcceptingCommands ahead of the message loop's RTS handshake), but the new routing was applied to Dash too. origin/main funnels every CMD write to cmdInput unconditionally and consumes it from there in both readMessage and ReadCmd. With the O5 dispatcher in place, a Dash phone's single-byte RTS still landed on cmdInput (matching the new control-byte fast-path), but multi-byte Dash writes drained to cmdActivation while readMessage was waiting on cmdInput — and ReadCmd was waiting on cmdActivation. Both sides starved. Mode-branch the dispatcher and ReadCmd: - HandleWriteFunc: in O5 mode, keep the current routing (multi-byte or non-RTS/SUCCESS/NACK/FAIL → cmdActivation; the four control bytes → cmdInput). In Dash mode, push every byte to cmdInput exactly like origin/main. - ReadCmd: in O5 mode, read from cmdActivation. In Dash mode, read from cmdInput. Both branches are documented inline so the asymmetry is explicit. With Commit D's loopDash dispatching cmdInput → readMessage and Commit C's Dash GATT only registering the 1A7E2441 cmd characteristic, the full Dash CMD path is now: phone write → cmdInput → loopDash readMessage, matching origin/main exactly.
Real Omnipod 5 pods include a 7-byte manufacturer-data field
(60 03 00 01 00 00 00) in their BLE advertisement; OmnipodKit's scanner
keys off that payload to recognise a pod, so an advertisement that only
carries name + service UUIDs is invisible to the controller. The Dash
port left this as a known gap because the vendored paypal/gatt fork did
not expose a helper for emitting manufacturer-data alongside name and
services.
Cherry-pick the AdvertiseNameServicesMfgData helper from jwoglom/five's
paypal/gatt fork:
- vendor/github.com/paypal/gatt/device.go: add the method declaration
to the public Device interface.
- vendor/github.com/paypal/gatt/device_linux.go: full Linux impl —
builds an AdvPacket with general-discoverable flags, fits the UUIDs,
appends typeManufacturerData, and spills the name to scan-response
only when it doesn't fit in the primary packet.
- vendor/github.com/paypal/gatt/device_darwin.go: shim that forwards
name + UUIDs (the upstream macOS XPC path does not surface a
mfg-data slot; matches five's behavior). Pi/Linux is the only target
where this matters for real-pod parity.
vendor/github.com/paypal/gatt/adv.go is deliberately NOT touched — the
local upstream-revert hack that bypasses the UUID-fit check stays
exactly as on the current branch.
Wire it up in pkg/bluetooth/bluetooth.go:
- advertiseO5 and refreshO5 now call AdvertiseNameServicesMfgData with
a package-level o5MfgData = []byte{0x60, 0x03, 0x00, 0x01, 0x00, 0x00,
0x00} so the wire bytes match OmnipodKit captures.
- advertiseDash and refreshDash are untouched — they keep
AdvertiseNameAndServices exactly like origin/main, so Dash btmon
captures still compare cleanly with main.
- The startup log gains a mfg_data=<hex> field so a Pi btmon trace can
correlate against the in-process record. Dash logs an empty value.
Commit 7 of the original O5 port made the response layer mode-aware by
persisting state.Mode and reading it on every command. Commit 7 also
unconditionally rewrote state.Mode = pairMode on every pod.New call,
which combined with main.go's -mode flag defaulting to "dash" produced
silent state corruption:
1. ./pod -fresh -mode o5 state.toml: mode = "o5"
2. ./pod flag = "dash" (default)
pod.New: state.Mode = ModeDash
state.toml rewritten to "dash"
BLE now advertises Dash; the pod
starts returning Dash response
bytes for SetUniqueID/GetVersion
against an O5 controller.
Introduce pod.ResolveMode(state, flag, fresh) -> (resolved, conflict)
and call it from main.go before bluetooth.New so the BLE profile is
chosen from the reconciled mode, not the raw flag. Precedence:
- fresh start: the flag wins (caller persists it).
- restart, state.Mode matches flag: no warning, no write.
- restart, state.Mode differs from flag: use persisted value, warn
loudly, do NOT rewrite state.toml. The operator can pass -fresh to
force a reset.
- restart, legacy state file with no mode field: TOML decodes Mode as
the zero value (ModeDash); matches the default flag, no warning.
pod.New now only persists state.Mode on freshState. On a restart it
trusts the resolved pairMode for in-memory routing and leaves the
persisted value alone. A defensive "argument != persisted" check
remains as belt-and-suspenders: it logs a loud warning if some future
caller forgets to ResolveMode first, but still never silently
overwrites the file.
Tests in pkg/pod/state_test.go cover all seven ResolveMode permutations
and round-trip pod.New through an O5 state.toml with the default Dash
flag, asserting the file is preserved verbatim.
This is the last functional commit in the BLE repair series (A-G).
Commit H will add the BLE-profile regression tests.
Add programmatic guards so a future O5-side change can't silently
reintroduce the Dash advertising / GATT / mfg-data regressions the
previous commit series fixed.
pkg/bluetooth/bluetooth.go (structural refactor only, no behavior
change): introduce a narrow `advertiser` interface that pulls
AdvertiseNameAndServices and AdvertiseNameServicesMfgData out of the
gatt.Device shape. The helpers advertiseDash / advertiseO5 /
refreshDash / refreshO5 now take this interface; gatt.Device
satisfies it implicitly, so the production call sites pass the
device unchanged. This makes the helpers testable without standing up
a real BLE stack.
pkg/bluetooth/profile_test.go (new): six table-driven tests assert
the exact wire bytes each mode produces.
- TestAdvertiseDashBytes / TestAdvertiseDashDefaultPodId: name is
" :: Fake POD ::", 9 UUIDs in main's exact order (0x4024, 0x2470,
0x000a, podIdOne, podIdTwo, 0x0814, 0x6DB1, 0x0006, 0xE451),
AdvertiseNameAndServices is the method called (not the mfg form),
default podId mapping is 0xffff / 0xfffe.
- TestAdvertiseO5Bytes / TestAdvertiseO5DefaultPodId: name is
"AP <PODID> 0A95B6110002761B", exactly 2 UUIDs
(CE1F923D-...-0A<podid>00 and ECF301E2-674B-4474-94D0-364F3AA653E6),
AdvertiseNameServicesMfgData is the method called, mfg payload is
{0x60, 0x03, 0x00, 0x01, 0x00, 0x00, 0x00}, default podId is
FFFFFFFE.
- TestRefreshDashBytes / TestRefreshO5Bytes: same shape for the
post-SetUniqueID refresh path.
pkg/aid/aid_test.go: extend the existing TestIsAIDPayload table with
four Dash SLPE prefixes (SET_UNIQUE_ID, GET_VERSION, GET_STATUS,
PROGRAM_INSULIN — all starting with "S0.0=") and assert
IsAIDPayload returns false. Guards against any future aid.go change
that would misclassify a Dash command into the AID branch.
End of the BLE repair series.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.