Skip to content

Omnipod Five simulator v2 - #24

Open
jwoglom wants to merge 17 commits into
LoopKit:mainfrom
jwoglom:omnipod-five-v2
Open

jwoglom wants to merge 17 commits into
LoopKit:mainfrom
jwoglom:omnipod-five-v2

Conversation

@jwoglom

@jwoglom jwoglom commented Oct 23, 2026 •

Copy link
Copy Markdown

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.

  • add -mode flag for selecting either o5 or dash -- still defaults to dash. The mode is saved in state.toml, so a restart keeps the pod the phone paired with (a different -mode is ignored with a warning; -fresh makes a new pod)
  • O5 link layer: 244-byte packets with no RTS/CTS, the O5 advertisement (CE1F923D-C539-48EA-7300-0A<controller id>00, 02 as the last byte on a fault) and data characteristic 1A7E2443, signed (type 4) messages, and the Omnipod CRC16 on responses
  • O5 pairing as OmnipodKit's O5LTKExchanger runs it (SPS0, P-256 SPS1, SPS2.1/SPS2 with certificates and signatures), then the AID setup commands; bolus, basal, temp basal, cancel and deactivate must be signed, or the pod answers error 0x2A as a real one does; version responses report product id 5
  • add helper (cmd/o5keypair) for generating an O5Data.swift file which defines simulated certificate data accepted by the simulator backend so the actual pairing process can occur
  • the control panel and GET /status show the mode, the controller and how many signed commands were rejected
  • Linux Bluetooth (Raspberry Pi): the pod logs the ATT MTU the phone negotiates, and asks for 256 when it is under the 247 an O5 packet needs (an iPhone offered 185, so SPS2.1 was cut short); and each L2CAP payload's fragments are now written together, as concurrent CMD/DATA notifications interleaved and the phone dropped both
  • Upgrade golang.org/x/sys library: the 2020 version crashed on its first log line on macOS with current Go. The September 2021 version is the newest that builds at this module's go 1.15, which is unchanged
  • rebased on Answer a stale EAP-AKA SQN with a synchronization failure #25: EAP-AKA uses its CheckSqn; this adds a random pod IV per session and a test of CheckSqn with the AUTN OmnipodKit sent
  • docs: docs/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

  • Loop in the iOS Simulator (LoopWorkspace 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.
  • Over the air on Linux (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/gatt carries in-place patches (128-bit UUID and manufacturer data advertising, the MTU request, the fragment lock). go mod vendor overwrites them, so restore those files after re-vendoring.
  • Outside vendor/ the diff is 52 files (+5,058/−394). In vendor/, everything but those gatt files and modules.txt is the mechanical x/sys update.
  • The over-the-air test runs only with the ble_e2e build tag, so CI skips it.

claude and others added 14 commits October 26, 2026 20:25
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
@marionbarker

Copy link
Copy Markdown

Test

❌ Fail to pair with OmnipodKit set for Omnipod 5
✅ Successful pair, prime, insert and get green loop with OmnipodKit set for DASH

Configuration

Trio instance, v1.0.2.30.
rPi was connected using pod main branch as DASH.

Deactivate the DASH.

add jwoglom as a remote and switch to jwoglom:omnipod-five-v2, commit 11b610f

Issue these commands:

go build
sudo setcap 'cap_net_raw,cap_net_admin=eip' ./pod

Attempt Omnipod 5 pairing with this branch

Trio phone, switch Pod Type to Omnipod 5.
Try to pair.

Am I calling it correctly?

 ./pod -mode o5 -fresh

The phone says: Peripheral not ready.
The rPi says:

2026/10/26 20:59:44 0x0007	0x2902	0x0E	0x00	*gatt.Descriptor	[ 00 00 ]
pkg bluetooth; ** New connection from:  2e:a9:90:e5:12:7e
INFO[2026-10-26T20:59:46-07:00] pkg bluetooth; the phone offered an ATT MTU of 185; asking it for 256 
INFO[2026-10-26T20:59:48-07:00] pkg pod; got first command: as string: 060104002a24d0 
INFO[2026-10-26T20:59:48-07:00] pkg pod; HELLO from controller 002a24d0      
DEBU[2026-10-26T20:59:48-07:00] Saving state to file: state.toml             
DEBU[2026-10-26T21:00:48-07:00] ReadMessage timeout

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.

marion@labpi:~/pod$ ./pod -mode o5 -fresh
INFO[2026-10-26T21:05:47-07:00] simulating an Omnipod 5 pod                  
2026/10/26 21:05:47 dev: hci0 up
2026/10/26 21:05:47 dev: hci0 reset
2026/10/26 21:05:47 dev: hci0 down
2026/10/26 21:05:47 dev: hci0 opened
INFO[2026-10-26T21:05:48-07:00] Starting API                                 
state: PoweredOn
INFO[2026-10-26T21:05:48-07:00] Pod simulator control panel on http://localhost:8080/ 
2026/10/26 21:05:48 Generating attribute table:
2026/10/26 21:05:48 handle	type	props	secure	pvt	value
INFO[2026-10-26T21:05:48-07:00] pkg pod; Listening for commands              
2026/10/26 21:05:48 0x0001	0x2800	0x02	0x00	*gatt.Service	[ 5F DC D0 03 1E 75 7E 8B 64 44 ED E3 24 40 7E 1A ]
2026/10/26 21:05:48 0x0002	0x2803	0x3C	0x00	*gatt.Characteristic	[ 3C 03 00 5F DC D0 03 1E 75 7E 8B 64 44 ED E3 41 24 7E 1A ]
2026/10/26 21:05:48 0x0003	0x1a7e2441e3ed44648b7e751e03d0dc5f	0x3C	0x00	*gatt.Characteristic	[  ]
2026/10/26 21:05:48 0x0004	0x2902	0x0E	0x00	*gatt.Descriptor	[ 00 00 ]
2026/10/26 21:05:48 0x0005	0x2803	0x3C	0x00	*gatt.Characteristic	[ 3C 06 00 5F DC D0 03 1E 75 7E 8B 64 44 ED E3 43 24 7E 1A ]
2026/10/26 21:05:48 0x0006	0x1a7e2443e3ed44648b7e751e03d0dc5f	0x3C	0x00	*gatt.Characteristic	[  ]
2026/10/26 21:05:48 0x0007	0x2902	0x0E	0x00	*gatt.Descriptor	[ 00 00 ]
pkg bluetooth; ** New connection from:  48:2d:b1:c0:55:4c
INFO[2026-10-26T21:05:48-07:00] pkg bluetooth; the phone offered an ATT MTU of 185; asking it for 256 
INFO[2026-10-26T21:05:54-07:00] pkg pod; got first command: as string: 060104002a24d0 
INFO[2026-10-26T21:05:54-07:00] pkg pod; HELLO from controller 002a24d0      
DEBU[2026-10-26T21:05:54-07:00] Saving state to file: state.toml             
INFO[2026-10-26T21:05:54-07:00] pkg bluetooth; handling CMD notifications on new connection from:  48:2d:b1:c0:55:4c 
INFO[2026-10-26T21:05:54-07:00] pkg bluetooth; handling DATA notifications on new connection from: 48:2d:b1:c0:55:4c 
WARN[2026-10-26T21:05:54-07:00] pkg bluetooth; the phone negotiated an ATT MTU of 185, but an Omnipod 5 pod's packets need 247: messages longer than one packet will not get through 
INFO[2026-10-26T21:05:54-07:00]      *** OK to send commands from the phone app *** 
INFO[2026-10-26T21:05:54-07:00] Received SP1 SP2 payload 5350313d0004002a24d12c5350323d000b002a24d100030e010002c5 
INFO[2026-10-26T21:05:54-07:00] Received SP1 SP2: 002a24d1 :: 002a24d100030e010002c5 
INFO[2026-10-26T21:05:54-07:00] Received SPS0 000109a218                     
DEBU[2026-10-26T21:05:54-07:00] Generated SPS0: 535053303d00050000099129     
INFO[2026-10-26T21:05:54-07:00] Received SPS1  2924dcc43212e31c5f520c47cce1e4b5b6906b2cf7a95706fbd9378335b79a4956e41f6f094142461fffba42e8bbae3eb2e10d6bf7820ec913d15ee5d3ac0e714eb681369432132f88d0f294b99145e8 
DEBU[2026-10-26T21:05:54-07:00] O5 conf ce444c3eca34a3ff3c31877476a0c2fa     
DEBU[2026-10-26T21:05:54-07:00] Pod public 46d9bd55800242276309a59ef8fd1732250dfac13fa91949f510b6e226bdf8cf0f3aa0166d0cd8dcea0c3bae64c98133a14c2748d9280b81db4ef404f48aa0d7 :: 64 
DEBU[2026-10-26T21:05:54-07:00] Pod nonce e14ac790fca397e81f2d8ecc0c1799d8 :: 16 
DEBU[2026-10-26T21:05:54-07:00] Generated SPS1: 535053313d005046d9bd55800242276309a59ef8fd1732250dfac13fa91949f510b6e226bdf8cf0f3aa0166d0cd8dcea0c3bae64c98133a14c2748d9280b81db4ef404f48aa0d7e14ac790fca397e81f2d8ecc0c1799d8 
DEBU[2026-10-26T21:06:16-07:00] pkg bluetooth; ignoring command 060104002a24d0 outside a message 
DEBU[2026-10-26T21:06:16-07:00] Message :(*message.Message)(0x40000ca000)({
 Eqos: (uint16) 0,
 Ack: (bool) false,
 Priority: (bool) false,
 LastMessage: (bool) false,
 Gateway: (bool) false,
 Type: (message.MessageType) 3,
 EncryptedPayload: (bool) false,
 Source: ([]uint8) (len=4 cap=40) {
  00000000  00 2a 24 d0                                       |.*$.|
 },
 Destination: ([]uint8) (len=4 cap=36) {
  00000000  ff ff ff fe                                       |....|
 },
 Payload: ([]uint8) (len=28 cap=32) {
  00000000  53 50 31 3d 00 04 00 2a  24 d1 2c 53 50 32 3d 00  |SP1=...*$.,SP2=.|
  00000010  0b 00 2a 24 d1 00 03 0e  01 00 02 c5              |..*$........|
 },
 Raw: ([]uint8) (len=44 cap=48) {
  00000000  54 57 10 03 01 00 03 80  00 2a 24 d0 ff ff ff fe  |TW.......*$.....|
  00000010  53 50 31 3d 00 04 00 2a  24 d1 2c 53 50 32 3d 00  |SP1=...*$.,SP2=.|
  00000020  0b 00 2a 24 d1 00 03 0e  01 00 02 c5              |..*$........|
 },
 Sas: (bool) true,
 Tfs: (bool) false,
 SequenceNumber: (uint8) 1,
 AckNumber: (uint8) 0,
 Version: (uint16) 0,
 Signature: ([]uint8) <nil>
}) 
ERRO[2026-10-26T21:06:16-07:00] pkg pod; error parsing SPS2.1 Name not found SPS2.1= in 5350313d0004002a24d12c5350323d000b002a24d100030e010002c5; hanging up 
INFO[2026-10-26T21:06:16-07:00] pkg pod; Listening for commands              
pkg bluetooth; ** New connection from:  48:2d:b1:c0:55:4c
INFO[2026-10-26T21:06:17-07:00] pkg bluetooth; the phone offered an ATT MTU of 185; asking it for 256 

Last attempt

quit trio
^C at rPi
cycle bluetooth on phone
open Trio
pair pod
start rPi

gets to here then says possible Bluetooth issue

DEBU[2026-10-26T21:12:03-07:00] Generated SPS1: 535053313d005055f4065461bb63d56372a7f7314822443702e0ddd6c98cf52279804147cebff4e198bceab9e0bd5716202228c1240d23be4d5f6856336be384085a8d447275ad7e5df9fc42997e709d0d2ff3c2fb2375 

Attempt DASH pairing with this branch

Trio phone - cancel the Omnipod 5 attempt, switch pod type to DASH

./pod -mode dash -fresh

✅ successful pair, prime and insert using this branch and dash mode

jwoglom and others added 3 commits October 27, 2026 09:23
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
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.

3 participants