Repository navigation
Conversation
Lays out what OmnipodKit (the authoritative O5 driver) expects of a pod, why the old five branch no longer pairs with it, and which package owns each O5 difference on this branch. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
The simulator can play an Omnipod DASH (the default) or an Omnipod 5. The mode is fixed when the pod is created with -fresh and saved in state.toml, so a restart without -mode keeps the pod the phone paired with; a differing flag is a warning. The link layer and the status page know the mode; nothing else changes yet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
The link layer now follows the pod's mode (OmnipodKit BlePodProfile, PodAdvertisement, PeripheralManager+OmnipodKit): * One split/join implementation, a port of OmnipodKit's PayloadSplitter and PayloadJoiner, with a 20-byte layout for DASH and a 244-byte one for an Omnipod 5. The DASH packets are what the simulator always wrote, except that the last one is zero padded as a pod's is; a packet out of order is NACKed with the index the pod expected. * An Omnipod 5 phone writes its packets without RTS/CTS, so in O5 mode the first data packet starts a message and the pod writes its own packets straight away; SUCCESS/NACK/FAIL stay. * An Omnipod 5 advertises one 128-bit UUID carrying the controller id (FFFFFFFE until paired, last byte 02 on a fault) and serves its data on characteristic 2443. Both radios do this; the vendored paypal/gatt learns to advertise a 128-bit UUID under its own AD type and to add manufacturer data. DASH advertising is unchanged byte for byte. * Message type 4 (encrypted and signed) is parsed, with its 64-byte signature, and the 11-bit length no longer truncates over 255 bytes. * The Omnipod message CRC16 and CRC-16/XMODEM, which the O5 protocol uses, with OmnipodKit's test vector. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
In O5 mode the pod runs OmnipodKit's O5LTKExchanger exchange: SPS0 (algorithm 09 with a CRC-16/XMODEM), SPS1 with P-256 keys, then SPS2.1 and SPS2 under the conf key both sides derive with SHA-256 over FIRMWARE_ID, the public keys and the ECDH secret. SPS2.1 carries each side's certificate and SPS2 its certificate and a signature over the 171-byte channel-binding transcript, AES-CCM encrypted with the per-direction counting nonces. The pod signs with a persistent P-256 identity in a self-signed certificate, which is all OmnipodKit reads, and keeps the phone's key from its SPS2 certificate for the signed (type 4) commands that follow. The DASH exchange is unchanged; a test checks it against OmnipodKit's DashKeyExchange, and another runs the whole O5 pairing against a Go model of the phone. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
* The pod pairs with the O5 exchange in O5 mode, keeps a persistent signing identity, takes the phone's id from its HELLO and advertises it once paired. * Between AssignAddress and SetupPod an Omnipod 5 phone sends plain-text AID setup commands (UTC, TDI, target profile, DIA, EGV, insulin history); pkg/aid parses and answers them with the values the phone set, checked against OmnipodKit's O5CommLogFixtures. * Insulin, cancel and deactivate commands must be signed (type 4) on an Omnipod 5; the signature over header and ciphertext is checked with the phone's key from SPS2, and an unsigned or badly signed one gets error 0x2A as a real pod answers. * The two version responses are built from the pod's identity, so an Omnipod 5 reports product id 5, its firmware and lot (OmnipodKit's captured values); DASH bytes are unchanged. O5 responses carry the Omnipod CRC16, which OmnipodKit verifies for O5 pods. * EAP-AKA draws a random pod IV each session (main checks AUTN in CheckSqn). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
…e mode A Go model of OmnipodKit's phone pairs with the pod over an in-memory link (DashLTKExchanger or O5LTKExchanger), opens an EAP-AKA session, sends AssignAddress, the Omnipod 5 AID setup, SetupPod, a status read and a bolus, checks the advertisement the pod switches to, and reconnects for a new session. On the Omnipod 5 an unsigned bolus is refused with 0x2A and a signed one runs; its responses carry a valid CRC16. The control panel names the pod's mode and controller, and the README documents -mode o5. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
docs/O5_SIMULATOR_VALIDATION.md walks a Mac through running Loop (LoopKit/LoopWorkspace next-dev, whose OmnipodKit talks to a simulated pod when ImpossiBLE is present) against this pod in -mode o5 through ImpossiBLE, and what to look for. cmd/o5keypair makes the Omnipod 5 registration data OmnipodKit needs before it will pair, as the o5keypair JSON its import takes or as an O5Data.swift that compiles the data into OmnipodKit; App Attest, which fetches real data, does not work in the Simulator. The data pairs with the simulator only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoKwZHxWpWRGKw3KNrgZfn
Built with current Go on macOS, the pod crashed with SIGSEGV on its first log line: logrus's terminal check calls x/sys's ioctl, and the 2020 x/sys hands libSystem a bad function pointer under the current toolchain. The September 2021 x/sys calls libSystem through trampoline addresses instead and is the newest that builds at this module's go 1.15 (later ones use unsafe.Slice). Only x/sys is re-vendored; the patched vendor/github.com/paypal/gatt files are kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
main's CheckSqn now reads the controller's SQN from AUTN and verifies MAC-A over it, which this commit did for the warning it replaced; what is left is its test, on the AUTN OmnipodKit sent in its first session after pairing with the simulator: it verifies with SQN 1, is fresh after a pairing, stale once 1 has been used, and does not verify with a corrupted MAC-A. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
An Omnipod 5 packet is 244 bytes, so the phone's writes need an ATT MTU of at least 247. The phone proposes the MTU, and paypal/gatt accepts it up to 256; below 247, single-packet messages still get through but a longer one (such as SPS2.1 when pairing) never arrives whole, and the phone times out waiting for SUCCESS with nothing in the pod's log. The Linux radio now logs the MTU when the DATA notifications are set up and whenever it changes, and warns when the pod's packets do not fit it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
The pod saves state.toml once it reads the phone's ACK, which can come after the test already has its response, so t.TempDir's cleanup could race that save and fail with "directory not empty" (about 7% of runs of TestAPairedPodTakesANewSession, which ends on a command). startPod now registers a cleanup, run before t.TempDir's, that waits for the pod's lock: the pod holds it from a command until the save after its ACK. It gives up after 3 s, as a test that failed mid-exchange can leave the pod waiting for an ACK. 0 failures in 1,200 runs, with Go 1.21 and 1.27. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
An iPhone pairing with the pod over Linux Bluetooth offered an ATT MTU of 185, so the phone's writes held 182 bytes and the Omnipod 5's 244-byte packets were truncated: SPS2.1 never arrived and pairing timed out. The phone starts the exchange as it connects, but either side may start one as the ATT client. In O5 mode the pod now waits for the phone's exchange and, if the MTU is still under 247, asks for 256 (the most paypal/gatt takes); the phone answers with its own Rx MTU and the MTU becomes the smaller of the two. The vendored paypal/gatt gains RequestMTU, takes the MTU response, and no longer answers an ATT error response with one of its own. DASH is unchanged. Not yet tried with a phone: the MTU logged after this exchange shows whether iOS accepts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
paypal/gatt cuts every L2CAP payload into 27-byte HCI ACL fragments
(its bufSize) and wrote them without a lock. The pod notifies DATA and
CMD from different goroutines, so with an Omnipod 5's 247-byte
notifications the SUCCESS on CMD landed between the fragments of a DATA
packet; the phone's controller dropped both ("Unexpected start frame"),
and pairing stopped at the pod's SPS2.1. DASH packets fit one fragment,
which is why it never showed. Each payload's fragments are now written
under a per-connection lock.
Found with an over-the-air test in a Linux VM (two btvirt controllers,
the pod on one, OmnipodKit's phone model on the other): with this and
the MTU request, an Omnipod 5 pairs, takes a session, the AID setup and
a signed bolus where the phone offers an MTU of 185; without it, the
pod's reply never arrives.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
TestOverTheAir (build tag ble_e2e, so go test ./... and CI skip it) runs the phone model from phone_test.go against the pod binary on another Bluetooth controller, over the Linux Bluetooth stack: the path an iPhone takes to a pod on a Raspberry Pi. The phone starts its own MTU exchange and answers the pod's as a scenario says, and cuts writes to MTU-3 bytes as iOS does. scripts/ble-e2e provisions an Ubuntu VM (Lima on a Mac) with BlueZ's btvirt, builds the binaries and runs eight scenarios against what each should do; docs/BLE_E2E_TEST.md explains it and the emulator's quirks the scripts work around. It found the two fixes before this one: all eight pass here, and the pod at ba86841 fails as a Pi did with an iPhone offering an MTU of 185. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
8af8dc1 to
11b610f
Compare
Test❌ Fail to pair with OmnipodKit set for Omnipod 5 ConfigurationTrio instance, v1.0.2.30. Deactivate the DASH. add jwoglom as a remote and switch to jwoglom:omnipod-five-v2, commit 11b610f Issue these commands: Attempt Omnipod 5 pairing with this branchTrio phone, switch Pod Type to Omnipod 5. Am I calling it correctly? The phone says: Peripheral not ready. Cycle Bluetooth, ^C the rPi and start again with pair and fresh commands. Got a little further this time, but it halted at Possible Bluetooth Issue. Cycled Bluetooth and hit retry and got more from rPi before I got Possible Bluetooth Issue again. Last attempt quit trio gets to here then says possible Bluetooth issue Attempt DASH pairing with this branchTrio phone - cancel the Omnipod 5 attempt, switch pod type to DASH ✅ successful pair, prime and insert using this branch and dash mode |
gatt answered Prepare Write and Execute Write with "not supported". iOS sends a write with response longer than MTU-3 as a long write, so the pod now queues the parts (up to 512 bytes, an attribute's most) and writes each handle's joined value on Execute Write, as a Write Request. It also logs how the phone answered the pod's MTU request, or that it refused it, which a Pi's log did not show. Long writes do not rescue an Omnipod 5 at an MTU of 185: they carry the phone's packets, but the pod's 244-byte notifications are still cut to 182 bytes, as no notification is longer than MTU-3. The over-the-air test's phone can send long writes (PHONE_FALLBACK), and two new scenarios, o5-185-185-long and o5-185-error-long, show this. The phone now records its own MTU exchange's answer as it reads it, so a later answer to the pod's request is not overwritten. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
A Pi's pod failed to pair with an iPhone that negotiated an ATT MTU of
185, as an Omnipod 5's 244-byte packets need 247; the iOS Simulator
(ImpossiBLE) has no MTU, so the failure could not be seen there.
-sim-mtu N gives the app that MTU on connecting ("mtu" in didConnect,
which g7sim's ImpossiBLE patch 0008 takes: it cuts writes without
response to N-3 bytes, as CoreBluetooth does), and cuts the pod's
notifications to N-3 bytes, as a peripheral is held to. With 185, Trio
v1.0.2.30 fails at SPS2.1 as on the Pi; with no MTU it pairs, primes,
inserts and deactivates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
gatt resets the controller as it starts, which leaves LE Data Length Extension off: link-layer packets of 27 bytes, where a Pi 3B+, 4 or 5 can send 251. An iPhone gave a Pi's pod an ATT MTU of 185, which is seven 27-byte packets less the L2CAP header, while an Omnipod 5's 244-byte packets need 247, one 251-byte packet. Whether the iPhone offers more on a link with longer packets is to be seen on a Pi. After the reset the pod reads the controller's longest packets, makes them the default, asks for them on each connection (LE Set Data Length), and writes ACL data in the controller's buffer size rather than 27 bytes. It logs what it set up and each Data Length Change, so a Pi's log shows the link the phone agreed to. A controller without the extension (a Pi 3B's is Bluetooth 4.1) is logged and left as before. The over-the-air test's emulator has no Data Length Extension; setup-vm.sh now adds it as a Pi 4's controller answers. All ten scenarios do what they should with it (ACL data in its 192-byte pieces) and without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013YkbWyt285vXZMpvHCbpLq
Port and improve upon the existing, but only partially-working O5 simulator which was forked from this repo earlier this year during O5 OmnipodKit development.
-modeflag for selecting either o5 or dash -- still defaults to dash. The mode is saved instate.toml, so a restart keeps the pod the phone paired with (a different-modeis ignored with a warning;-freshmakes a new pod)CE1F923D-C539-48EA-7300-0A<controller id>00,02as the last byte on a fault) and data characteristic1A7E2443, signed (type 4) messages, and the Omnipod CRC16 on responses0x2Aas a real one does; version responses report product id 5cmd/o5keypair) for generating an O5Data.swift file which defines simulated certificate data accepted by the simulator backend so the actual pairing process can occurGET /statusshow the mode, the controller and how many signed commands were rejectedgo 1.15, which is unchangedCheckSqn; this adds a random pod IV per session and a test ofCheckSqnwith the AUTN OmnipodKit sentdocs/O5_SIMULATOR_VALIDATION.md(Loop in the iOS Simulator),docs/BLE_E2E_TEST.md(over-the-air test on Linux),docs/O5_PORT_PLAN.md(protocol details and their OmnipodKit sources)Tested with ImpossiBLE with Loop on an iOS simulator alongside the pod simulator in O5 mode.
Testing
next-dev, ImpossiBLE): O5 pairing with the signatures verified both ways, priming, cannula insertion, bolus, temp basal, suspend and resume, Read Pod Status, an alert and an occlusion fault from the control panel, deactivation; no signed command rejected, no AES-CCM or CRC errors. DASH: pairing through deactivation. Reconnecting after Loop relaunches needs an ImpossiBLE fix that is in jwoglom/g7sim (impossible-patches/0007) and not yet in LoopKit/g7sim. This run was before the rebase on Answer a stale EAP-AKA SQN with a synchronization failure #25.docs/BLE_E2E_TEST.md, after the rebase): the pod binary on one virtual Bluetooth controller, the Go model of OmnipodKit's phone on another, acting as an iPhone that offers an MTU of 185. An O5 pod pairs and runs (session, AID setup, SetupPod, status, an unsigned bolus refused, a signed bolus); DASH is unchanged; all 8 scenarios do what they should. The pod before the MTU request fails as a Pi did with that iPhone. Whether a real iPhone grants the pod's MTU request is still to be confirmed on a Pi.go test ./...with Go 1.21 (as CI) and 1.27, including a Go model of OmnipodKit's phone that runs the whole O5 and DASH flows in memory.Notes for reviewers
vendor/github.com/paypal/gattcarries in-place patches (128-bit UUID and manufacturer data advertising, the MTU request, the fragment lock).go mod vendoroverwrites them, so restore those files after re-vendoring.vendor/the diff is 52 files (+5,058/−394). Invendor/, everything but those gatt files andmodules.txtis the mechanical x/sys update.ble_e2ebuild tag, so CI skips it.