Repository navigation
Conversation
-mode o5 serves an Omnipod 5 instead of a DASH, as OmnipodKit's O5 path
expects one. The mode is kept in the state file so restarting does not
change the pod the phone paired with.
The link layer gains a packet layout, so the framing OmnipodKit's
PayloadSplitter and PayloadJoiner describe is written once and used at
both packet sizes rather than only for a DASH's 20-byte packets. An
Omnipod 5 uses 244-byte packets, skips the RTS/CTS handshake, serves the
data characteristic 1A7E2443, and advertises one 128-bit service UUID
holding its controller's id, with the trailing byte it flips when it
wants attention.
Pairing follows O5LTKExchanger: SPS0, then P-256 ECDH and a SHA-256 key
derivation in SPS1, then two certificate rounds under AES-CCM in which
each side shows an X.509 certificate and signs a transcript of what both
have said. The pod makes itself a self-signed certificate; as in
OmnipodKit, neither side validates a chain, so each reads the public key
out of the other's certificate to check its signature. The exchange ends
in the long-term key and the EAP-AKA session a DASH already used.
A pod then answers the setup commands the phone sends before SetupPod,
reports product id 5 and a lot number that decodes as an Omnipod 5, and
refuses an insulin, cancel or deactivate command that did not arrive
signed by its controller with error 0x2A, as a real pod does.
Along the way:
- the message length field is read as the 11 bits it is, not a byte,
which wrapped on a pairing message over 255 bytes
- CRC16 is computed rather than left as zero, since an Omnipod 5's
phone checks it; verified against OmnipodKit's test vector and four
captured pod messages
- the control panel shows the pod type, the controller, and any
command refused for want of a signature
- cmd/o5keypair writes the registration data the app needs, which it
cannot download in the iOS Simulator
-mode o5 needs -transport impossible: the Linux stack here advertises
only 16-bit UUIDs and does not negotiate the MTU an Omnipod 5 needs, so
it now says so instead of advertising something no phone will answer.
OmnipodKit acknowledges an Omnipod 5 setup command's answer exactly as it acknowledges any other response: sendO5AidCommand ends by incrementing its sequence and nonce and sending an empty ACK. The pod was not reading it, so the next time round the loop it took that ACK for a command, found no command in it and exited — which took its radio with it, leaving the app reporting that Bluetooth was gone rather than the message the pod could not read. So an AID exchange costs three nonces like any other, not two. The acknowledgement read is now one helper used by both paths. A command the pod cannot read no longer ends the process either. It hangs up and lets the phone set up a new session, so the failure shows up as the logged message that caused it instead of as a dead radio.
The simulator hangs up on an idle link after three minutes, which mimics the DASH pods that drop a connection every few minutes so an iOS hook can use it as a heartbeat. That is a running pod's behaviour. A pod being activated was doing it too, in the middle of the user filling, priming, attaching and inserting it — all of which takes longer than three minutes and none of which a real pod interrupts. So only a pod that has finished activating drops an idle link. This also keeps the activation clear of a reconnect, which does not currently recover: the phone reconnects at the BLE level but no session is established on the new connection, and after an app relaunch it scans for the unpaired advertisement rather than the one its paired pod sends.
The pod refused -mode o5 on a Bluetooth adapter because the vendored paypal/gatt could not advertise the 128-bit service UUID an Omnipod 5 identifies itself by. It turns out it nearly could: AppendUUIDFit had the length-aware packing commented out, and what was left wrote every UUID into a 16-bit list whatever its length, so a 128-bit UUID went out mislabelled and no phone matched it. Restoring that packing needed its size accounting corrected as well. UUIDs of one length share a single field, so the two bytes of field header are paid once per list and not once per UUID; counting it per UUID made nine 16-bit UUIDs look 8 bytes too large for the packet and turned a DASH's complete list into an incomplete one. A second fix next to it: the branch that moves the name into the scan response declared a second AdvPacket that shadowed the one being built, so it filled in the scan response and then advertised the shadowed-away packet. It only showed once something did not fit, which is every advertisement carrying a 128-bit UUID — an Omnipod 5's fills 21 of the 31 bytes and leaves no room for the name. Both are marked in the vendored files, since `go mod vendor` overwrites them. The radio now takes its service and characteristic UUIDs from the pod profile rather than the DASH ones, so -mode o5 serves 1A7E2443 for data. No MTU work: the phone drives that exchange and the pod answers with its own maximum, which gatt already clamps to 256 — comfortably above the 247 a 244-byte packet needs. The pod logs what the two settled on instead, and says so loudly if an Omnipod 5 ends up below 247.
main gained the recovery from #23 and the EAP-AKA SQN check from #25 while this branch was out. Three things had to be reconciled rather than merged mechanically. The sequence number pairing leaves behind moved from 1 to 0, because CheckSqn accepts a session only if its SQN is past the stored one and the controller's first session uses 1. The Omnipod 5 path was still storing 1, which would have made every freshly paired pod answer its phone's first session with a synchronization failure. There is now a test over the AUTN a controller sends, covering the first session after pairing, a replay and an older session, so the rule is pinned rather than remembered. The setup commands an Omnipod 5 is sent now run before the command-length guard, not after. Their payloads are short ASCII — "G3.12" is five bytes, "S3.9=8,G3.9" eleven — so the sixteen-byte minimum #23 moved ahead of command.Unmarshal would have refused them and hung up mid-activation. The Omnipod 5 pairing and the AID exchange now hang up through startOver like everything else, instead of ending the process. That was the behaviour this branch wanted anyway: the pod's own commit for it is now redundant and gives way to #23's. The link layer keeps this branch's framing, which already returned the errors #23 introduced, and upstream's pod_test.go is carried over onto the mode-aware constructors.
A pod made itself a fresh self-signed certificate for each pairing, so there was nothing stable about it: an app could only ever fail to recognise a simulated pod, never identify one. Pods are now signed by a fixed authority whose key is in the repository on purpose, the way g7sim publishes its CA so Loop can mark a simulated G7 positively. It is a development marker and proves nothing about authenticity. That also makes the exchange faithful. A pod now shows the authority in SPS2.1 and its own leaf in SPS2, which is the shape the controller already uses, where SPS2.1 carries the intermediate and SPS2 the leaf. A real pod does the same: its SPS2.1 is Insulet's INS01PG1 and its SPS2 a leaf issued by it. The name follows a real pod too. One advertises itself as "AP FFFFFFFE 0E977731000A3AB7" — the controller it is paired to, then its lot and tid. The lot decodes as product 7, a U100 Omnipod 5, which is what the simulator already reports in its version responses, so the two now agree. A DASH keeps the name this simulator has always had. cmd/o5keypair -ca prints the authority for an app to embed. The chain test deliberately pins no validity date: a real pod's certificate was found nineteen months expired and pairing normally, so an authenticity check that honoured notAfter would flag every pod in the field.
|
@ps2 I still don't think this is working: OmnipodKit logspod -mode o5 -q -fresh |
|
Works great for me on Raspberry Pi 4 Model B. It looks like you have a 4.0/4.1 era ble adapter. That means no LE Data Length Extension and it won't work. Run |
|
I'll add a log to the code to make this fatal on hardware that won't work, so the reason is clear. |
Test on an rPi 4❌ fails using Omnipod 5 Pod Type with OmnipodKit I see Pete's comment. Tried with both nomimal OmnipodKit (Trio instance) and with o5-pod-authenticity branch (Loop next-dev instance). $ git log --oneline -3 Did the build twice to make sure I'm using the new code using commands: Xcode log for failed pairingcommand on rPi Pair Pod on the phone. Xcode debug log: |
|
Looks like you have HCI Version: 4.1, too, @marionbarker. That hardware won't work to simulate O5. Do you have a different/newer rpi to try? |
…pter be named An Omnipod 5 packet is 244 bytes and needs a 247-byte ATT MTU. A phone asks for one that large only of a Bluetooth 4.2 or later radio and asks for 185 of anything older, so a Pi 3B's own 4.1 radio cannot serve one. The phone fragments a message into packets but never fragments a packet, so on a short link every write is silently truncated: pairing used to get through SP1/SP2, SPS0 and SPS1 — all of which fit — and then stall at SPS2.1, the first message that needs more than one packet, leaving the phone to report a timeout with nothing in the pod log to explain it. The pod now refuses such a link as soon as the phone has written, naming what to do about it, instead of warning at connect time and hanging later. Radio grows LinkMTU, which gatt answers from the central and ImpossiBLE answers 0 to, having no ATT MTU of its own, and Ble.MTUShortfall turns that into the refusal using the profile's own packet size, so a DASH computes 23 and never trips. -hci names the adapter to serve from, since a 4.2+ dongle in an older Pi will not be hci0 and gatt otherwise takes the first adapter with LE support. The README covers which boards work, how to check a radio and what to try.
Two boards both called a Pi 4 were found side by side, one pairing an Omnipod 5 and one failing at SPS2.1: one reports Cypress 5.0 revision 0x17e, the other Broadcom 4.1 revision 0x0. So the board model predicts nothing about the radio, and the reported version is what has to be measured. Such a gap has two causes needing opposite remedies — a genuinely older Broadcom part, or the Cypress one with firmware missing or outdated, which still comes up and works but without the 4.2 features a 247-byte ATT MTU depends on. Say how to tell them apart from dmesg, which names the part and whether the patch loaded, and give the remedy for each.
|
Thanks both — between your two logs the failure itself is pinned down, though the reason your radios differ isn't yet. What's failingIt's the ATT MTU, not the pairing protocol. Both logs break in exactly the same place:
An Omnipod 5 packet is 244 bytes, so it needs a 247-byte ATT MTU. James's pod log recorded what his phone actually negotiated: At 185 the write ceiling is 182 bytes, and OmnipodKit notes the consequence itself in Why one Pi 4 works and another doesn't — not yet settledThe radios report differently, and the board name doesn't predict it: A phone asks for a 247+ MTU only of a 4.2-or-later radio and asks for 185 of anything
@marionbarker, two commands tell them apart — If that shows (The kernel asks for a device-tree-specific name first, e.g. @jwoglom, same two commands would be useful from your host. If your radio is genuinely Either way, Changes on the branch
One caveat: I've also suggested |
…omits it A Pi 4 that could not pair an Omnipod 5 turned out to hold the same Cypress BCM4345C0 as one that could, running ROM because none of the four patch filenames the kernel tries was present: not an older radio, and not an outdated patch, but no patch at all. Its distribution was Ubuntu, which unlike Raspberry Pi OS does not carry the file, so the advice to update bluez-firmware was no use there. Name the dmesg lines that show a radio on ROM, say the file is distribution dependent, and give the one command that puts it in place anywhere.
|
That settles it, @marionbarker — and I owe you a correction: your radio is the same part as mine, so ignore my earlier suggestion about a newer Pi. You don't need different hardware.
The reason my Grab it and reboot: Then check it took — you want Cypress, 4.2 or later, and a revision that isn't For comparison, the working Pi here reports: The plain filename is in the kernel's try-list, so there's no need for a model-specific copy. Once that reads 4.2+, re-run the pod (remember |
Test✅ success with Omnipod 5 after updating the rPi. rPi 4Initial condition: Download and reboot: After reboot |
-mode o5makes the simulator serve an Omnipod 5 instead of a DASH, as OmnipodKit'sO5 path expects one. The mode is kept in the state file, so restarting does not change
the pod the phone paired with; a different
-modeis ignored with a warning and-freshstarts a new pod. DASH is unchanged and still the default.What an Omnipod 5 does differently
CE1F923D-C539-48EA-7300-0A<controller id>00, carrying thecontroller it paired with (
FFFFFFFEbefore pairing, trailing02while the podwants attention, which is how a backgrounded app hears about a fault).
1A7E2443rather than1A7E2442.received it answers SUCCESS.
two rounds under AES-CCM in which each side shows an X.509 certificate and signs a
transcript of what both have said — ending in the same long-term key and EAP-AKA
session a DASH already used.
or signed by anything but the controller the pod paired with, is refused with error
0x2Aas a real pod refuses it, and the control panel counts it.The link layer now carries a packet layout, so the framing OmnipodKit's
PayloadSplitterand
PayloadJoinerdescribe is written once and used at both packet sizes rather thanonly for a DASH's 20-byte packets.
Two bugs found on the way
arithmetic, so it wrapped on any payload over 255 bytes — every O5 pairing message.
crc.CRC16returned a hardcoded zero. An O5's phone checks it (checkCRC: isO5).Now computed, and verified against OmnipodKit's own
CRC16Testsvector plus fourcaptured pod messages from its
MessageTests.Linux Bluetooth
The vendored
paypal/gattneeded two repairs, both marked in-file sincego mod vendoroverwrites them:
AppendUUIDFithad its length-aware packing commented out, and what remained wroteevery UUID into a 16-bit list whatever its length — so a 128-bit UUID went out
mislabelled and no phone matched it. Restoring it needed the size accounting corrected:
UUIDs of one length share a field, so the two header bytes are paid once per list, not
once per UUID. Counting per UUID made nine 16-bit UUIDs look 8 bytes too large and
downgraded a DASH's complete list to incomplete.
AdvPacketthat shadowed the one being built, so it filled in the scan response and then
advertised the shadowed-away packet. Latent until something did not fit, which is every
advertisement carrying a 128-bit UUID.
No MTU work. The phone drives that exchange and the pod answers with its own maximum,
which gatt already clamps to 256 — above the 247 a 244-byte packet needs. The pod logs
what the two settled on and says so loudly if an O5 lands below 247.
Registration data
OmnipodKit will not begin an O5 pairing without a controller signing key and certificate
chain, which on a phone it downloads via App Attest — impossible in the iOS Simulator.
cmd/o5keypairwrites a self-signed set (-swiftfor anO5Data.swiftto build intoOmnipodKit, or JSON). It pairs with this simulator and nothing else, since neither side
validates a chain.
Verification
iOS Simulator (Loop
next-dev, ImpossiBLE): full activation — pairing with signaturesverifying both ways, the nine AID commands, SetupPod, priming, cannula insertion, pod
running with basal active. No signed command rejected, no AES-CCM or CRC error.
Real hardware (iPhone 11 on iOS 18.7.1, pod on a Raspberry Pi 4 over real Bluetooth):
full activation on the first attempt. ATT MTU 256 negotiated, three signed insulin
commands verified, zero errors.
btmonconfirms the advertisement goes out as128-bit Service UUIDs (complete): 1 entry — ce1f923d-c539-48ea-7300-0afffffffe00with the name in the scan response.
go test ./...green. Outsidevendor/the diff is 32 files, +3,781/−243; the vendordiff is 2 files and 70 lines.
Relationship to #24
This overlaps @jwoglom's #24, which implements the same feature. It was written
independently from OmnipodKit's Swift source rather than derived from that PR, and does
not borrow from it. Worth noting what #24 has that this does not: an over-the-air test
harness running on virtual Bluetooth controllers, and a Go model of OmnipodKit's phone —
real regression coverage, including the MTU-constrained case a real iPhone does not
exercise. Whichever is preferred, that harness is worth keeping.