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
jwoglom
force-pushed
the
omnipod-five-v2
branch
from
October 27, 2026 02:27
8af8dc1 to
11b610f
Compare
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.
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.