Skip to content

Simulate an Omnipod 5 as well as a DASH - #26

Open
ps2 wants to merge 9 commits into
mainfrom
o5-sim-support
Open

ps2 wants to merge 9 commits into
mainfrom
o5-sim-support

Conversation

@ps2

@ps2 ps2 commented Oct 28, 2026

Copy link
Copy Markdown

-mode o5 makes the simulator serve 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; a different -mode is ignored with a warning and
-fresh starts a new pod. DASH is unchanged and still the default.

What an Omnipod 5 does differently

  • One 128-bit service UUID, CE1F923D-C539-48EA-7300-0A<controller id>00, carrying the
    controller it paired with (FFFFFFFE before pairing, trailing 02 while the pod
    wants attention, which is how a backgrounded app hears about a fault).
  • Data characteristic 1A7E2443 rather than 1A7E2442.
  • 244-byte packets and no RTS/CTS: a message begins with its first packet, and whoever
    received it answers SUCCESS.
  • A longer pairing exchange — SPS0, P-256 ECDH and a SHA-256 derivation in SPS1, then
    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.
  • The AID setup commands the phone sends between pairing and SetupPod.
  • Product id 5 and a lot number that decodes as an Omnipod 5.
  • A real CRC16 on every response, which the phone checks for an O5 and ignores for a DASH.
  • A signature on every command that starts or stops insulin. One that arrives unsigned,
    or signed by anything but the controller the pod paired with, is refused with error
    0x2A as a real pod refuses it, and the control panel counts it.

The link layer now carries 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.

Two bugs found on the way

  • The message length field is 11 bits across two header bytes and was being read in byte
    arithmetic, so it wrapped on any payload over 255 bytes — every O5 pairing message.
  • crc.CRC16 returned a hardcoded zero. An O5's phone checks it (checkCRC: isO5).
    Now computed, and verified against OmnipodKit's own CRC16Tests vector plus four
    captured pod messages from its MessageTests.

Linux Bluetooth

The vendored paypal/gatt needed two repairs, both marked in-file since go mod vendor
overwrites them:

  • AppendUUIDFit had its length-aware packing commented out, and what remained 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 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.
  • 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. 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/o5keypair writes a self-signed set (-swift for an O5Data.swift to build into
OmnipodKit, 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 signatures
verifying 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. btmon confirms the advertisement goes out as
128-bit Service UUIDs (complete): 1 entry — ce1f923d-c539-48ea-7300-0afffffffe00
with the name in the scan response.

go test ./... green. Outside vendor/ the diff is 32 files, +3,781/−243; the vendor
diff 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.

ps2 added 4 commits October 27, 2026 16:56
-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.
@ps2
ps2 marked this pull request as ready for review October 28, 2026 00:11
ps2 added 2 commits October 27, 2026 19:16
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.
@jwoglom

jwoglom commented Oct 28, 2026

Copy link
Copy Markdown

@ps2 I still don't think this is working:

OmnipodKit logs

O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.00s rssi=-26 state=0 connectable=Optional(1) name=AP FFFFFFFE 0E146DB10006E451 svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Ignoring paired or unconnectable peripheral: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>
centralManager(_:didDiscover:advertisementData:rssi:): <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>, {
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataLocalName = "AP FFFFFFFE 0E146DB10006E451";
    kCBAdvDataRxPrimaryPHY = 1;
    kCBAdvDataRxSecondaryPHY = 0;
    kCBAdvDataServiceUUIDs =     (
        "CE1F923D-C539-48EA-7300-0AFFFFFFFE00"
    );
    kCBAdvDataTimestamp = "814850893.657714";
}
O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.16s rssi=-34 state=0 connectable=Optional(1) name=AP FFFFFFFE 0E146DB10006E451 svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Ignoring paired or unconnectable peripheral: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>
centralManager(_:didDiscover:advertisementData:rssi:): <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>, {
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataLocalName = "AP FFFFFFFE 0E146DB10006E451";
    kCBAdvDataRxPrimaryPHY = 1;
    kCBAdvDataRxSecondaryPHY = 0;
    kCBAdvDataServiceUUIDs =     (
        "CE1F923D-C539-48EA-7300-0AFFFFFFFE00"
    );
    kCBAdvDataTimestamp = "814850893.657761";
}
O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.00s rssi=-34 state=0 connectable=Optional(1) name=AP FFFFFFFE 0E146DB10006E451 svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Ignoring paired or unconnectable peripheral: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>
centralManager(_:didDiscover:advertisementData:rssi:): <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>, {
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataLocalName = "AP FFFFFFFE 0E146DB10006E451";
    kCBAdvDataRxPrimaryPHY = 1;
    kCBAdvDataRxSecondaryPHY = 0;
    kCBAdvDataServiceUUIDs =     (
        "CE1F923D-C539-48EA-7300-0AFFFFFFFE00"
    );
    kCBAdvDataTimestamp = "814850893.817619";
}
O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.16s rssi=-27 state=0 connectable=Optional(1) name=AP FFFFFFFE 0E146DB10006E451 svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Ignoring paired or unconnectable peripheral: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>
centralManager(_:didDiscover:advertisementData:rssi:): <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>, {
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataLocalName = "AP FFFFFFFE 0E146DB10006E451";
    kCBAdvDataRxPrimaryPHY = 1;
    kCBAdvDataRxSecondaryPHY = 0;
    kCBAdvDataServiceUUIDs =     (
        "CE1F923D-C539-48EA-7300-0AFFFFFFFE00"
    );
    kCBAdvDataTimestamp = "814850893.8176759";
}
O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.00s rssi=-27 state=0 connectable=Optional(1) name=AP FFFFFFFE 0E146DB10006E451 svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Ignoring paired or unconnectable peripheral: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>
Pairing pod before priming
discoverPods()
Start scanning (discovery/pairing filter=CE1F923D-C539-48EA-7300-0AFFFFFFFE00)
connection (noPod   ) [pairing] discoverPods — scanning for a pairable pod
PersistedProperty.swift - wrappedValue - 111 DEV: Saved persisted value 'PumpManagerState'
connection (noPod   ) [pairing] scan started (filter=CE1F923D-C539-48EA-7300-0AFFFFFFFE00)
centralManager(_:didDiscover:advertisementData:rssi:): <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected>, {
    kCBAdvDataIsConnectable = 1;
    kCBAdvDataRxPrimaryPHY = 1;
    kCBAdvDataRxSecondaryPHY = 0;
    kCBAdvDataServiceUUIDs =     (
        "CE1F923D-C539-48EA-7300-0AFFFFFFFE00"
    );
    kCBAdvDataTimestamp = "814850893.97192";
}
O5 advertisement PDM id: 4294967294
[ADV] 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 dt=0.15s rssi=-30 state=0 connectable=Optional(1) name=- svcUUIDs=[CE1F923D-C539-48EA-7300-0AFFFFFFFE00] mfg=- svcData=-
O5 advertisement PDM id: 4294967294
Matched peripheral <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> to existing device: Omni - advertisement: Optional(OmnipodKit.PodAdvertisement(DASH_MAIN_SERVICE_UUID: "4024", DASH_UNKNOWN_THIRD_SERVICE_UUID: "000A", podType: Omnipod 5, serviceUUIDs: [CE1F923D-C539-48EA-7300-0AFFFFFFFE00]))
Connecting to pairable device <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 0, state = disconnected> in discovery mode
connection (noPod   ) [pairing] heard pod 766D0010-DC1E-C7E1-04FB-BEE16B8B7341 pairable=true state=0
connection (noPod   ) [pairing] connecting to pairable pod 766D0010-DC1E-C7E1-04FB-BEE16B8B7341
[#AAECF032] CONNECTED: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 23, state = connected> — connect latency 0.396s (known device: true)
PeripheralManager - didConnect: <CBPeripheral: 0x1169eb480, identifier = 766D0010-DC1E-C7E1-04FB-BEE16B8B7341, name = Tandem Mobi 123, mtu = 23, state = connected>
PeripheralManager - didConnect - running assertConfiguration
didReadRSSI: -28
connection (noPod   ) [connectOnDemand] connected in 0.396s
didDiscoverServices
Discovered service: <CBService: 0x123b02dc0, isPrimary = YES, UUID = 1A7E4024-E3ED-4464-8B7E-751E03D0DC5F>
Found pod UUID 766D0010-DC1E-C7E1-04FB-BEE16B8B7341!
@@@ Attempting to pair and setup pod using myId 0x2A05FC and podId 0x2A05FD
Scheduling session Pair and setup pod
======================== Pair and setup pod ===========================
PersistedProperty.swift - wrappedValue - 111 DEV: Saved persisted value 'PumpManagerState'
DeviceDataManager.swift - pumpManagerPumpWasReplaced(_:) - 360 INFO: Pump replaced
O5KeyExchange init: controllerId=002a05fc
O5 pairing attempt using fixed confirmed parameters
=== O5 Pairing Start === myId=0x2a05fc podId=0x2a05fd
Sending SP1+SP2
Encoded SP2 get status command for address 0x2a05fd and seq # 1: 002a05fd00030e010001ba
[o5throwOnSendError] SP1=,SP2=: begin (payload 28 bytes, seq 1). peripheral state=connected
[o5throwOnSendError] SP1=,SP2=: sentWithAcknowledgment (SUCCESS received). peripheral state=connected
Sending SPS0
Generated SPS0 value: 000109a218
[o5throwOnSendError] SPS0=: begin (payload 12 bytes, seq 2). peripheral state=connected
[o5throwOnSendError] SPS0=: sentWithAcknowledgment (SUCCESS received). peripheral state=connected
Reading SPS0
Received SPS0 from pod (12 raw bytes): 535053303d00050000099129
Sending SPS1 (80 bytes: pubkey=64 + nonce=16)
[o5throwOnSendError] SPS1=: begin (payload 87 bytes, seq 3). peripheral state=connected
[o5throwOnSendError] SPS1=: sentWithAcknowledgment (SUCCESS received). peripheral state=connected
Received SPS1 from pod (87 raw bytes): 535053313d00500a4168ce78f197e7213ef1ed22b662ff426646a63aaf195ae8b98d9a04ccfbc07c004462245fc7967462a4da738d66652809a291941507aeebe7329d815175e97d6c8b003bdbb5c4e1a0887ca9b557bb
SPS1 payload from pod: 80 bytes (expected 80)
  KDF input (210 bytes): 00000000000000069b0ab96a76f4000000000000000400000000000000000000004084797d7bef0b75302b4a850c6c26c08f0b406ff57545dd477a6a05e7ea4f7c64ba8821936c9140cec1f67a4f0b884e5fc11a83ece8dd31e3dc7f1a488b47625300000000000000400a4168ce78f197e7213ef1ed22b662ff426646a63aaf195ae8b98d9a04ccfbc07c004462245fc7967462a4da738d66652809a291941507aeebe7329d815175e90000000000000020a19941a99369a7706483f16e17b64ee1e3c819053a795a1353b0706c3c76aec7
Key derivation complete: conf=454d4f9cd6ebc20b1b0614906dc454a6, ltk=e18a28347de50c2b993f2d990fd07667
=== SPS2.1 PHASE START ===
Building SPS2.1 payload...
SPS2.1: using cert (634 bytes, cert-only short path)
Encrypting SPS2.1: key=454d4f9cd6ebc20b1b0614906dc454a6, nonce=01b5d00d1521937d6c8b003bdb, plaintext=634 bytes
SPS2.1 encrypted: 642 bytes (target=642)
SPS2.1 payload built: 642 bytes encrypted
Sending SPS2.1 (651 bytes)... peripheral state=connected
[o5throwOnSendError] SPS2.1=: begin (payload 651 bytes, seq 4). peripheral state=connected
[sendMessagePacket] Error (didSend=true): emptyValue. peripheral state=CBPeripheralState(rawValue: 2)
[o5throwOnSendError] SPS2.1=: sentWithError — data was sent but error occurred: emptyValue. peripheral state=connected
O5 pairing FAILED: pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)")
blePairAndSetupAndPrimePod failed with commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))
------------------------ Pair and setup pod ---------------------------
Start of idle at Optional(2026-10-28 03:28:21 +0000)
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
### Verbose Bluetooth comms error: Pairing Exception: Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)
### Verbose Bluetooth comms error: Pairing Exception: Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
OmniPairingError self pumpManagerError(LoopKit.PumpManagerError.communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))) got error communication(Optional(OmnipodKit.PodCommsError.commsError(error: OmnipodKit.PodProtocolError.pairingException("Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)"))))
### Verbose Bluetooth comms error: Pairing Exception: Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)
### Verbose Bluetooth comms error: Pairing Exception: Send SPS2.1= failure (seq 4, 651 bytes): sentWithError(OmnipodKit.PeripheralManagerError.emptyValue)
pod -mode o5 -q -fresh

2026-10-27T23:17:39-04:00 osaid pod-five[972]: INFO[2026-10-27T23:17:39-04:00] simulating an Omnipod 5 pod
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 dev: hci0
2026-10-27T23:17:39-04:00 osaid pod-five[972]:  up
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 dev: hci0
2026-10-27T23:17:39-04:00 osaid pod-five[972]:  down
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 dev: hci0
2026-10-27T23:17:39-04:00 osaid pod-five[972]:  opened
2026-10-27T23:17:39-04:00 osaid pod-five[972]: INFO[2026-10-27T23:17:39-04:00] Starting API
2026-10-27T23:17:39-04:00 osaid pod-five[972]: state: PoweredOn
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 Generating attribute table:
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 handle        type        props        secure        pvt        value
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 0x0001        0x2800        0x02        0x00        *gatt.Service        [ 5F DC D0 03 1E 75 7E 8B 64 44 ED E3 24 40 7E 1A ]
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 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-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 0x0003        0x1a7e2441e3ed44648b7e751e03d0dc5f        0x3C        0x00        *gatt.Characteristic        [  ]
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 0x0004        0x2902        0x0E        0x00        *gatt.Descriptor        [ 00 00 ]
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 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-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 0x0006        0x1a7e2443e3ed44648b7e751e03d0dc5f        0x3C        0x00        *gatt.Characteristic        [  ]
2026-10-27T23:17:39-04:00 osaid pod-five[972]: 2026/10/27 23:17:39 0x0007        0x2902        0x0E        0x00        *gatt.Descriptor        [ 00 00 ]
2026-10-27T23:17:39-04:00 osaid pod-five[972]: INFO[2026-10-27T23:17:39-04:00] Pod simulator control panel on http://localhost:8084/
2026-10-27T23:17:39-04:00 osaid pod-five[972]: INFO[2026-10-27T23:17:39-04:00] pkg pod; Listening for commands
2026-10-27T23:28:13-04:00 osaid pod-five[972]: pkg bluetooth; ** New connection from:  84:00:e3:15:70:79
2026-10-27T23:28:13-04:00 osaid pod-five[972]: WARN[2026-10-27T23:28:13-04:00] pkg bluetooth; the phone negotiated an ATT MTU of 185, below the 247 an Omnipod 5 packet needs; pairing will truncate
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pod; got first command: as string: 060104002a05fc
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pod; HELLO from controller 002a05fc
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg bluetooth; handling CMD notifications on new connection from:  84:00:e3:15:70:79
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg bluetooth; handling DATA notifications on new connection from: 84:00:e3:15:70:79
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00]      *** OK to send commands from the phone app ***
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pair; Omnipod 5 pairing for pod 002a05fd from controller 002a05fc
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pod; Received SP1SP2
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pod; Received SPS0
2026-10-27T23:28:15-04:00 osaid pod-five[972]: INFO[2026-10-27T23:28:15-04:00] pkg pod; Received SPS1

@ps2

ps2 commented Oct 28, 2026

Copy link
Copy Markdown
Author

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 hciconfig hci0 version and check out the HCI Version line.

@ps2

ps2 commented Oct 28, 2026

Copy link
Copy Markdown
Author

I'll add a log to the code to make this fatal on hardware that won't work, so the reason is clear.

@marionbarker

Copy link
Copy Markdown

Test on an rPi 4

❌ fails using Omnipod 5 Pod Type with OmnipodKit
✅ succeeds using DASH Pod Type with OmnipodKit

I see Pete's comment.
$ hciconfig hci0 version
hci0: Type: Primary Bus: UART
BD Address: AA:AA:AA:AA:AA:AA ACL MTU: 1021:8 SCO MTU: 64:1
HCI Version: 4.1 (0x7) Revision: 0x0
LMP Version: 4.1 (0x7) Subversion: 0x6119
Manufacturer: Broadcom Corporation (15)

Tried with both nomimal OmnipodKit (Trio instance) and with o5-pod-authenticity branch (Loop next-dev instance).

$ git log --oneline -3
6eb3f09 (HEAD -> o5-sim-support, origin/o5-sim-support) Serve every pod from one published authority, and name it as a pod does

Did the build twice to make sure I'm using the new code using commands:

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

Xcode log for failed pairing

command on rPi

 ./pod -mode o5 -fresh

Pair Pod on the phone.

Xcode debug log:

@ps2

ps2 commented Oct 28, 2026 •

Copy link
Copy Markdown
Author

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?

ps2 added 2 commits October 27, 2026 23:20
…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.
@ps2

ps2 commented Oct 28, 2026

Copy link
Copy Markdown
Author

Thanks both — between your two logs the failure itself is pinned down, though the reason your radios differ isn't yet.

What's failing

It's the ATT MTU, not the pairing protocol. Both logs break in exactly the same place:

@jwoglom @marionbarker
SP1+SP2 / SPS0 / SPS1 28 / 12 / 87 bytes ✅ 28 / 12 / 87 bytes ✅
SPS2.1 651 bytes ❌ emptyValue 651 bytes ❌ emptyValue

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:

WARN the phone negotiated an ATT MTU of 185, below the 247 an Omnipod 5 packet needs

At 185 the write ceiling is 182 bytes, and OmnipodKit notes the consequence itself in PeripheralManager.swift: "For O5 withoutResponse, any writes exceeding maximumWriteValueLength are silently truncated — NOT fragmented." The phone fragments a message into packets but never fragments a packet. SP1/SP2, SPS0 and SPS1 all fit under 182, which is why pairing appeared to get most of the way; SPS2.1 is the first message needing more than one packet, so its first fragment is cut from 244 to 182, the pod's joiner waits for the rest forever, and the phone times out as emptyValue. Everything before that point — the 128-bit advertisement, the AP FFFFFFFE … name, SPS0/SPS1, the P-256 ECDH — worked on both setups.

Why one Pi 4 works and another doesn't — not yet settled

The radios report differently, and the board name doesn't predict it:

HCI Version: 5.0 (0x9)  Revision: 0x17e   Manufacturer: Cypress Semiconductor (305)   # pairs here
HCI Version: 4.1 (0x7)  Revision: 0x0     Manufacturer: Broadcom Corporation (15)     # @marionbarker

A phone asks for a 247+ MTU only of a 4.2-or-later radio and asks for 185 of anything
older, so 4.1 is enough to explain the failure. What it doesn't tell us is why it reports
4.1, and the two possibilities need opposite fixes:

  • a genuinely older Broadcom part (BCM43430A1/BCM43438, the Pi 3B / Zero W radio) — then
    it's hardware, and a dongle or another board is the answer
  • the Cypress part (BCM4345C0, in a 3B+/4/5/CM4) with its firmware patch missing or
    outdated — it still comes up and works, just without the 4.2 features, and that's a free fix

@marionbarker, two commands tell them apart — dmesg is the decisive one, since it names
the part and whether the patch loaded:

cat /proc/device-tree/model; grep Revision /proc/cpuinfo
sudo dmesg | grep -iE "bcm|bluetooth|firmware"

If that shows BCM4345C0, try the firmware before buying anything:

ls -l /lib/firmware/brcm/BCM4345C0*.hcd
dpkg -s bluez-firmware | grep -i Version
sudo apt update && sudo apt full-upgrade && sudo reboot
hciconfig hci0 version        # want 4.2 or later

(The kernel asks for a device-tree-specific name first, e.g.
brcm/BCM4345C0.raspberrypi,4-model-b.hcd, then the plain brcm/BCM4345C0.hcd. If dmesg
says the specific one is missing while the plain file exists, copy it to the name it asked
for.) Apologies for my earlier "get a newer rpi" — that was premature; it may still be the
answer, but the readings above should decide it rather than me guessing.

@jwoglom, same two commands would be useful from your host. If your radio is genuinely
pre-4.2, a 4.2+ dongle is the fix; it won't be hci0, so use the new -hci flag:

hciconfig            # find it, usually hci1
./pod -mode o5 -fresh -hci 1

Either way, -transport impossible against the iOS Simulator has no ATT MTU at all, so it
sidesteps the radio entirely.

Changes on the branch

  • the pod now refuses a link too small to carry a packet, as soon as the phone has written, naming what to do about it — instead of warning at connect time and stalling at SPS2.1 with nothing in the pod log to explain it
  • -hci N selects the adapter, since a dongle in a Pi won't be hci0 and the first LE-capable adapter was being taken
  • README covers how to judge a radio by the version it reports rather than the board it's on

One caveat: I've also suggested sudo hcitool -i hci0 cmd 0x08 0x0024 fb 00 48 03 (LE Write Suggested Default Data Length) for a 4.2+ radio that still settles on 185. That's untested against a real case — if someone tries it and it changes nothing, say so and I'll drop it from the docs.

…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.
@ps2

ps2 commented Oct 28, 2026

Copy link
Copy Markdown
Author

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.

Bluetooth: hci0: BCM4345C0 (003.001.025) build 0000
Bluetooth: hci0: BCM: firmware Patch file not found, tried:
  'brcm/BCM4345C0.raspberrypi,4-model-b.hcd'
  'brcm/BCM4345C0.hcd'
  'brcm/BCM.raspberrypi,4-model-b.hcd'
  'brcm/BCM.hcd'

BCM4345C0 is the Cypress radio in every Pi 3B+/4/5/CM4, including the one pairing fine here. But all four filenames the kernel tries are absent, so it's running ROM — which still comes up and works, just reporting itself as Broadcom 4.1 revision 0x0 without the 4.2 features a 247-byte ATT MTU needs. build 0000 is the giveaway.

The reason my bluez-firmware tip didn't apply: your kernel command line (root=LABEL=writable, fixrtc) says Ubuntu, and Ubuntu's Pi images don't ship Broadcom's Bluetooth firmware at all, whereas Raspberry Pi OS does. Your WiFi hit the same model-specific miss and fell back to the generic .bin; Bluetooth had no file to fall back to.

Grab it and reboot:

sudo mkdir -p /lib/firmware/brcm
sudo curl -fsSL -o /lib/firmware/brcm/BCM4345C0.hcd \
  https://github.com/RPi-Distro/bluez-firmware/raw/master/broadcom/BCM4345C0.hcd
sudo reboot

Then check it took — you want Cypress, 4.2 or later, and a revision that isn't 0x0:

hciconfig hci0 version

For comparison, the working Pi here reports:

HCI Version: 5.0 (0x9)  Revision: 0x17e  Manufacturer: Cypress Semiconductor (305)

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 setcap after a rebuild) and O5 pairing should go through — and if the MTU is still short for any reason, the branch now says so outright instead of stalling at SPS2.1.

@marionbarker

marionbarker commented Oct 28, 2026 •

Copy link
Copy Markdown

Test

✅ success with Omnipod 5 after updating the rPi.
✅ paired, primed, inserted
Thanks Pete

rPi 4

Initial condition:

$ hciconfig hci0 version
hci0:	Type: Primary  Bus: UART
	BD Address: AA:AA:AA:AA:AA:AA  ACL MTU: 1021:8  SCO MTU: 64:1
	HCI Version: 4.1 (0x7)  Revision: 0x0
	LMP Version: 4.1 (0x7)  Subversion: 0x6119
	Manufacturer: Broadcom Corporation (15)

Download and reboot:

sudo mkdir -p /lib/firmware/brcm
sudo curl -fsSL -o /lib/firmware/brcm/BCM4345C0.hcd \
  https://github.com/RPi-Distro/bluez-firmware/raw/master/broadcom/BCM4345C0.hcd
sudo reboot

After reboot

marion@labpi:~$ hciconfig hci0 version
hci0:	Type: Primary  Bus: UART
	BD Address: E4:5F:01:2B:DC:56  ACL MTU: 1021:8  SCO MTU: 64:1
	HCI Version: 5.0 (0x9)  Revision: 0x17e
	LMP Version: 5.0 (0x9)  Subversion: 0x6119
	Manufacturer: Cypress Semiconductor (305)

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