From a3df2d9e08b80169f98d4fe29f5efc1c238a4296 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 12 Oct 2026 10:02:37 -0500 Subject: [PATCH 1/4] Reject a garbled message instead of exiting The pod told the phone the transfer failed, then exited, relying on run.sh to restart it. With nothing to restart it in the Simulator, a relaunched Loop that sent a message the pod read garbled lost its pod. The phone sends it again. --- pkg/bluetooth/bluetooth.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/bluetooth/bluetooth.go b/pkg/bluetooth/bluetooth.go index 8e6c6a7..82f406f 100644 --- a/pkg/bluetooth/bluetooth.go +++ b/pkg/bluetooth/bluetooth.go @@ -247,7 +247,9 @@ func (b *Ble) loop(stop chan bool) { case cmd := <-b.cmdInput: msg, err := b.readMessage(cmd) if err != nil { - log.Fatalf("pkg bluetooth; error reading message: %s", err) + // The phone was told it failed and sends it again. + log.Errorf("pkg bluetooth; error reading message: %s", err) + continue } b.messageInput <- msg } From 45b0fb0484b27871b52f6a4c7b4f4c919dd48456 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 12 Oct 2026 10:36:39 -0500 Subject: [PATCH 2/4] Advertise the pod's fault and alerts A paired pod's 2nd service UUID is C001 normally, C005 while an alert fires and C00A on a fault, and its manufacturer data carries a status word with the fault code and alert slots, as captured from real pods in OmnipodKit. The simulator advertised neither, so Loop's connectionless fault detection (a background scan for C00A, which wakes the app) never saw a simulated fault. Faults and alerts set from the control panel or by commands now show in the advertisement. paypal/gatt advertises no manufacturer data, so over Bluetooth only the UUID changes. --- README.md | 5 ++++ pkg/bluetooth/bluetooth.go | 46 +++++++++++++++++++++++++++++--- pkg/bluetooth/gatt_linux.go | 36 +++++++++++++++++++++---- pkg/bluetooth/impossible.go | 23 ++++++++++++---- pkg/bluetooth/impossible_test.go | 14 ++++++++-- pkg/pod/pod.go | 2 +- pkg/pod/status.go | 2 ++ 7 files changed, 112 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index cb2eec7..e1454ea 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,11 @@ impossible-mux /tmp/impossible-g7.sock /tmp/impossible-pod.sock The pod holds a connect that asks for a start delay (`CBConnectPeripheralOptionStartDelayKey`) for that delay, as iOS does, and hangs up after 3 minutes without a command, as above. +Its advertisement shows a fault or a firing alert, as a real pod's does: the 2nd service UUID becomes `C00A` on a fault +(`C005` for an alert), and the manufacturer data carries the fault code and alert slots. A fault set from the control +panel reaches a backgrounded Loop the way a real one does, through its scan for `C00A`; with `impossible-mux -suspend` +the advertisement wakes the suspended app. + # Original README.md We maintained the original README file below. It may be helpful if someone plans to cross-compile the code and just transfer the executable. diff --git a/pkg/bluetooth/bluetooth.go b/pkg/bluetooth/bluetooth.go index 82f406f..ba3218f 100644 --- a/pkg/bluetooth/bluetooth.go +++ b/pkg/bluetooth/bluetooth.go @@ -31,22 +31,57 @@ type Radio interface { NotifyCmd(Packet) error NotifyData(Packet) error Advertise(podId []byte) error + AdvertiseState(state AdvertState) error Disconnect() } const AdvertisedName = " :: Fake POD ::" +// AdvertState is what a paired pod advertises of its state: its fault +// code and its firing alert slots, 0 when there are none. +type AdvertState struct { + Fault uint8 + Alerts uint8 +} + +// status is the 2nd service UUID: real pods go C001 → C005 when an alert +// fires → C00A on a fault (captured in OmnipodKit). A fault is a new +// advertisement, which wakes a suspended app scanning for it. +func (s AdvertState) status() uint16 { + switch { + case s.Fault != 0: + return 0xC00A + case s.Alerts != 0: + return 0xC005 + } + return 0xC001 +} + +// ManufacturerData is the pod's iBeacon-shaped manufacturer data, with the +// status word OmnipodKit decodes 7 bytes from its end: 00, 02 (or the fault +// code), the fault code, the alert slots. A captured occlusion read 00141400. +func (s AdvertState) ManufacturerData() []byte { + b1 := uint8(0x02) + if s.Fault != 0 { + b1 = s.Fault + } + data := []byte{0x4c, 0x00, 0x02, 0x15} + data = append(data, make([]byte, 14)...) + return append(data, 0x00, b1, s.Fault, s.Alerts, 0x00, 0x00, 0xc5) +} + // AdvertisedServices is the list of 16-bit service UUIDs a DASH pod // advertises; the 4th and 5th carry its id, FFFF FFFE before pairing. -func AdvertisedServices(podId []byte) []uint16 { - idOne, idTwo := uint16(0xffff), uint16(0xfffe) +func AdvertisedServices(podId []byte, state AdvertState) []uint16 { + idOne, idTwo, second := uint16(0xffff), uint16(0xfffe), uint16(0x2470) if podId != nil { idOne = binary.BigEndian.Uint16(podId[0:2]) idTwo = binary.BigEndian.Uint16(podId[2:4]) + second = state.status() } return []uint16{ 0x4024, - 0x2470, + second, 0x000a, idOne, idTwo, @@ -164,6 +199,11 @@ func (b *Ble) RefreshAdvertisingWithSpecifiedId(id []byte) error { // 4 bytes, f return b.radio.Advertise(id) } +// SetAdvertisedState advertises the pod's fault and firing alerts. +func (b *Ble) SetAdvertisedState(state AdvertState) error { + return b.radio.AdvertiseState(state) +} + func (b *Ble) WriteCmd(packet Packet) error { b.cmdOutput <- packet diff --git a/pkg/bluetooth/gatt_linux.go b/pkg/bluetooth/gatt_linux.go index d518d5b..8456ffc 100644 --- a/pkg/bluetooth/gatt_linux.go +++ b/pkg/bluetooth/gatt_linux.go @@ -15,6 +15,10 @@ type gattRadio struct { device gatt.Device central gatt.Central + advertMtx sync.Mutex + podId []byte + state AdvertState + cmdNotifier gatt.Notifier cmdNotifierMtx sync.Mutex @@ -38,7 +42,7 @@ func NewGattRadio(b *Ble, podId []byte) (Radio, error) { if err != nil { log.Fatalf("pkg bluetooth; failed to open device, err: %s", err) } - r := &gattRadio{device: d} + r := &gattRadio{device: d, podId: podId} d.Handle( gatt.CentralConnected(func(c gatt.Central) { @@ -101,7 +105,7 @@ func NewGattRadio(b *Ble, podId []byte) (Radio, error) { log.Fatalf("pkg bluetooth; could not add service: %s", err) } - if err := r.advertise(podId); err != nil { + if err := r.advertise(); err != nil { log.Fatalf("pkg bluetooth; could not advertise: %s", err) } default: @@ -114,9 +118,13 @@ func NewGattRadio(b *Ble, podId []byte) (Radio, error) { return r, nil } -func (r *gattRadio) advertise(podId []byte) error { +func (r *gattRadio) advertise() error { + r.advertMtx.Lock() + podId, state := r.podId, r.state + r.advertMtx.Unlock() + // paypal/gatt advertises no manufacturer data. var uuids []gatt.UUID - for _, u := range AdvertisedServices(podId) { + for _, u := range AdvertisedServices(podId, state) { uuids = append(uuids, gatt.UUID16(u)) } return r.device.AdvertiseNameAndServices(AdvertisedName, uuids) @@ -124,7 +132,25 @@ func (r *gattRadio) advertise(podId []byte) error { // Advertise updates the advertisement; paypal/gatt needs no StopAdvertising first. func (r *gattRadio) Advertise(podId []byte) error { - err := r.advertise(podId) + r.advertMtx.Lock() + r.podId = podId + r.advertMtx.Unlock() + err := r.advertise() + if err != nil { + log.Infof("pkg bluetooth; could not re-advertise: %s", err) + } + return err +} + +func (r *gattRadio) AdvertiseState(state AdvertState) error { + r.advertMtx.Lock() + changed := r.state != state + r.state = state + r.advertMtx.Unlock() + if !changed { + return nil + } + err := r.advertise() if err != nil { log.Infof("pkg bluetooth; could not re-advertise: %s", err) } diff --git a/pkg/bluetooth/impossible.go b/pkg/bluetooth/impossible.go index b948f7b..75a8755 100644 --- a/pkg/bluetooth/impossible.go +++ b/pkg/bluetooth/impossible.go @@ -31,6 +31,7 @@ type simRadio struct { scanning bool connected bool podId []byte + state AdvertState notifying map[string]bool // characteristic ids delayed *time.Timer // a connect held for its start delay } @@ -283,20 +284,21 @@ func (r *simRadio) handle(c *simClient, msg map[string]interface{}) { // advertising, which it does whenever it has no connection. func (r *simRadio) discovered() { r.mu.Lock() - c, scanning, connected, podId := r.client, r.scanning, r.connected, r.podId + c, scanning, connected, podId, state := r.client, r.scanning, r.connected, r.podId, r.state r.mu.Unlock() if c == nil || !scanning || connected { return } var uuids []string - for _, u := range AdvertisedServices(podId) { + for _, u := range AdvertisedServices(podId, state) { uuids = append(uuids, fmt.Sprintf("%04X", u)) } c.send(map[string]interface{}{"type": "didDiscover", "id": r.id, "name": AdvertisedName, "rssi": -60, "adv": map[string]interface{}{ - "kCBAdvDataLocalName": AdvertisedName, - "kCBAdvDataIsConnectable": true, - "kCBAdvDataServiceUUIDs": uuids, + "kCBAdvDataLocalName": AdvertisedName, + "kCBAdvDataIsConnectable": true, + "kCBAdvDataServiceUUIDs": uuids, + "kCBAdvDataManufacturerData": base64.StdEncoding.EncodeToString(state.ManufacturerData()), }}) } @@ -385,6 +387,17 @@ func (r *simRadio) Advertise(podId []byte) error { return nil } +func (r *simRadio) AdvertiseState(state AdvertState) error { + r.mu.Lock() + changed := r.state != state + r.state = state + r.mu.Unlock() + if changed { + r.discovered() + } + return nil +} + // Disconnect is the pod hanging up. func (r *simRadio) Disconnect() { r.mu.Lock() diff --git a/pkg/bluetooth/impossible_test.go b/pkg/bluetooth/impossible_test.go index e9173c4..6e4dd53 100644 --- a/pkg/bluetooth/impossible_test.go +++ b/pkg/bluetooth/impossible_test.go @@ -2,6 +2,7 @@ package bluetooth import ( "bufio" + "bytes" "encoding/base64" "encoding/json" "io/ioutil" @@ -65,7 +66,7 @@ func TestSimRadioPairsAndCarriesBothCharacteristics(t *testing.T) { app.send(map[string]interface{}{"type": "scan"}) adv := app.expect("didDiscover")["adv"].(map[string]interface{}) uuids := adv["kCBAdvDataServiceUUIDs"].([]interface{}) - if len(uuids) != 9 || uuids[0] != "4024" || uuids[2] != "000A" || uuids[3] != "FFFF" || uuids[4] != "FFFE" { + if len(uuids) != 9 || uuids[0] != "4024" || uuids[1] != "2470" || uuids[2] != "000A" || uuids[3] != "FFFF" || uuids[4] != "FFFE" { t.Fatalf("an unpaired pod advertises %v", uuids) } @@ -124,9 +125,18 @@ func TestSimRadioPairsAndCarriesBothCharacteristics(t *testing.T) { t.Fatal("still connected") } uuids = app.expect("didDiscover")["adv"].(map[string]interface{})["kCBAdvDataServiceUUIDs"].([]interface{}) - if uuids[3] != "170D" || uuids[4] != "2A40" { + if uuids[1] != "C001" || uuids[3] != "170D" || uuids[4] != "2A40" { t.Fatalf("a paired pod advertises %v", uuids) } + + // A fault changes the advertisement, which wakes a suspended app. + b.SetAdvertisedState(AdvertState{Fault: 0x14}) + adv = app.expect("didDiscover")["adv"].(map[string]interface{}) + uuids = adv["kCBAdvDataServiceUUIDs"].([]interface{}) + mfg, _ := base64.StdEncoding.DecodeString(adv["kCBAdvDataManufacturerData"].(string)) + if uuids[1] != "C00A" || len(mfg) < 8 || !bytes.Equal(mfg[len(mfg)-7:len(mfg)-3], []byte{0x00, 0x14, 0x14, 0x00}) { + t.Fatalf("a pod with an occlusion advertises %v, %x", uuids, mfg) + } } func TestSimRadioHoldsAConnectForItsStartDelay(t *testing.T) { diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 207d153..a32bced 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -50,7 +50,7 @@ func New(ble *bluetooth.Ble, state *PODState) *Pod { ble: ble, state: state, } - ret.snapshot = *state + ret.takeSnapshot() return ret } diff --git a/pkg/pod/status.go b/pkg/pod/status.go index d70230c..031b21c 100644 --- a/pkg/pod/status.go +++ b/pkg/pod/status.go @@ -4,6 +4,7 @@ import ( "fmt" "time" + "github.com/avereha/pod/pkg/bluetooth" "github.com/avereha/pod/pkg/response" ) @@ -55,6 +56,7 @@ func (p *Pod) takeSnapshot() { p.snapMtx.Lock() p.snapshot = *p.state p.snapMtx.Unlock() + p.ble.SetAdvertisedState(bluetooth.AdvertState{Fault: p.state.FaultEvent, Alerts: p.state.ActiveAlertSlots}) } // Status reports the pod as of its last completed command or change. From f3038c1c594a03c475e790d654331d3305687a67 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 12 Oct 2026 12:02:20 -0500 Subject: [PATCH 3/4] Give each connection its own packets Connected() stopped the message loop, but a loop blocked inside a message only checked for that between messages. When the phone went away mid-message (an app suspended in the background), the old loop stayed blocked in ReadData and took the first packet of the next connection's next message. The new loop read the rest out of step, failed its checksum and told the phone FAIL, and every session after that failed the same way. Each connection now gets its own input channels, and the loop's reads end when it is stopped. --- pkg/bluetooth/bluetooth.go | 133 ++++++++++++++++++++++++-------- pkg/bluetooth/bluetooth_test.go | 106 +++++++++++++++++++++++++ 2 files changed, 206 insertions(+), 33 deletions(-) create mode 100644 pkg/bluetooth/bluetooth_test.go diff --git a/pkg/bluetooth/bluetooth.go b/pkg/bluetooth/bluetooth.go index ba3218f..db1b6a2 100644 --- a/pkg/bluetooth/bluetooth.go +++ b/pkg/bluetooth/bluetooth.go @@ -93,9 +93,22 @@ func AdvertisedServices(podId []byte, state AdvertState) []uint16 { } } +// input is one connection's writes from the phone. Each connection gets +// its own, so a reader left over from an earlier one never takes them. +type input struct { + cmd chan Packet + data chan Packet + closed chan bool // a newer connection replaced this one +} + +func newInput() *input { + return &input{cmd: make(chan Packet, 5), data: make(chan Packet, 5), closed: make(chan bool)} +} + +var errStopped = errors.New("the connection ended") + type Ble struct { - dataInput chan Packet - cmdInput chan Packet + in *input // guarded by genMtx dataOutput chan Packet cmdOutput chan Packet @@ -117,8 +130,7 @@ type Ble struct { // New starts the pod's link layer on the radio that attach returns. func New(attach func(*Ble) (Radio, error)) (*Ble, error) { b := &Ble{ - dataInput: make(chan Packet, 5), - cmdInput: make(chan Packet, 5), + in: newInput(), dataOutput: make(chan Packet, 5), cmdOutput: make(chan Packet, 5), messageInput: make(chan *message.Message, 5), @@ -151,16 +163,12 @@ func New(attach func(*Ble) (Radio, error)) (*Ble, error) { // Connected is the radio reporting a new connection from the phone. func (b *Ble) Connected() { b.StopMessageLoop() - // What the previous connection left unread is no part of the new one. - for _, ch := range []chan Packet{b.cmdInput, b.dataInput} { - for len(ch) > 0 { - <-ch - } - } for len(b.messageInput) > 0 { <-b.messageInput } b.genMtx.Lock() + close(b.in.closed) + b.in = newInput() b.connGen++ b.connected = true b.genMtx.Unlock() @@ -187,11 +195,28 @@ func (b *Ble) IsConnected() bool { // CmdWritten and DataWritten are the phone's writes to the two characteristics. func (b *Ble) CmdWritten(data []byte) { - b.cmdInput <- Packet(append([]byte(nil), data...)) + b.input().cmd <- Packet(append([]byte(nil), data...)) } func (b *Ble) DataWritten(data []byte) { - b.dataInput <- Packet(append([]byte(nil), data...)) + b.input().data <- Packet(append([]byte(nil), data...)) +} + +func (b *Ble) input() *input { + b.genMtx.Lock() + defer b.genMtx.Unlock() + return b.in +} + +// read takes the next packet from a connection's channel, or reports that +// the message loop reading it was stopped. +func read(ch chan Packet, stop chan bool) (Packet, error) { + select { + case p := <-ch: + return p, nil + case <-stop: + return nil, errStopped + } } func (b *Ble) RefreshAdvertisingWithSpecifiedId(id []byte) error { // 4 bytes, first 2 usually empty @@ -222,14 +247,24 @@ func (b *Ble) writeDataBuffer(buf *bytes.Buffer) error { return b.WriteData(data) } +// ReadCmd and ReadData read the current connection, following the phone +// to a new one. func (b *Ble) ReadCmd() (Packet, error) { - packet := <-b.cmdInput - return packet, nil + for { + in := b.input() + if p, err := read(in.cmd, in.closed); err == nil { + return p, nil + } + } } func (b *Ble) ReadData() (Packet, error) { - packet := <-b.dataInput - return packet, nil + for { + in := b.input() + if p, err := read(in.data, in.closed); err == nil { + return p, nil + } + } } func (p Packet) String() string { @@ -277,21 +312,35 @@ func (b *Ble) WriteMessage(message *message.Message) { b.messageOutput <- message } -func (b *Ble) loop(stop chan bool) { +func (b *Ble) loop(stop chan bool, in *input) { for { + select { + case <-stop: + return + default: + } select { case <-stop: return case msg := <-b.messageOutput: - b.writeMessage(msg) - case cmd := <-b.cmdInput: - msg, err := b.readMessage(cmd) + if err := b.writeMessage(msg, in, stop); err != nil { + return + } + case cmd := <-in.cmd: + msg, err := b.readMessage(cmd, in, stop) + if err == errStopped { + return + } if err != nil { // The phone was told it failed and sends it again. log.Errorf("pkg bluetooth; error reading message: %s", err) continue } - b.messageInput <- msg + select { + case b.messageInput <- msg: + case <-stop: + return + } } } } @@ -308,7 +357,7 @@ func (b *Ble) StartMessageLoop() { default: } b.stopLoop = make(chan bool) - go b.loop(b.stopLoop) + go b.loop(b.stopLoop, b.input()) } func (b *Ble) StopMessageLoop() { @@ -319,19 +368,25 @@ func (b *Ble) StopMessageLoop() { } } -func (b *Ble) expectCommand(expected Packet) { - cmd, _ := b.ReadCmd() +func (b *Ble) expectCommand(expected Packet, in *input, stop chan bool) error { + cmd, err := read(in.cmd, stop) + if err != nil { + return err + } if !bytes.Equal(expected[:1], cmd[:1]) { log.Fatalf("pkg bluetooth; expected command: %s. received command: %s", expected, cmd) } + return nil } -func (b *Ble) writeMessage(msg *message.Message) { +func (b *Ble) writeMessage(msg *message.Message, in *input, stop chan bool) error { var buf bytes.Buffer var index = 0 b.WriteCmd(CmdRTS) - b.expectCommand(CmdCTS) // TODO figure out what to do if !CTS + if err := b.expectCommand(CmdCTS, in, stop); err != nil { // TODO figure out what to do if !CTS + return err + } bytes, err := msg.Marshal() if err != nil { log.Fatalf("pkg bluetooth; could not marshal the message %s", err) @@ -360,7 +415,7 @@ func (b *Ble) writeMessage(msg *message.Message) { buf.Write(bytes[14:]) b.writeDataBuffer(&buf) } - return + return nil } size := len(bytes) @@ -404,10 +459,10 @@ func (b *Ble) writeMessage(msg *message.Message) { } b.writeDataBuffer(&buf) } - b.expectCommand(CmdSuccess) + return b.expectCommand(CmdSuccess, in, stop) } -func (b *Ble) readMessage(cmd Packet) (*message.Message, error) { +func (b *Ble) readMessage(cmd Packet, in *input, stop chan bool) (*message.Message, error) { var buf bytes.Buffer var checksum []byte @@ -419,7 +474,10 @@ func (b *Ble) readMessage(cmd Packet) (*message.Message, error) { b.WriteCmd(CmdCTS) - first, _ := b.ReadData() + first, err := read(in.data, stop) + if err != nil { + return nil, err + } fragments := int(first[1]) expectedIndex := 1 oneExtra := false @@ -436,7 +494,10 @@ func (b *Ble) readMessage(cmd Packet) (*message.Message, error) { buf.Write(first[2:20]) } for i := 1; i < fragments; i++ { - data, _ := b.ReadData() + data, err := read(in.data, stop) + if err != nil { + return nil, err + } if i == expectedIndex { buf.Write(data[1:20]) } else { @@ -448,7 +509,10 @@ func (b *Ble) readMessage(cmd Packet) (*message.Message, error) { expectedIndex++ } if fragments != 0 { - data, _ := b.ReadData() + data, err := read(in.data, stop) + if err != nil { + return nil, err + } len := data[1] if len > 14 { oneExtra = true @@ -459,7 +523,10 @@ func (b *Ble) readMessage(cmd Packet) (*message.Message, error) { } log.Tracef("pkg bluetooth; One extra: %t", oneExtra) if oneExtra { - data, _ := b.ReadData() + data, err := read(in.data, stop) + if err != nil { + return nil, err + } buf.Write(data[2 : data[1]+2]) } bytes := buf.Bytes() diff --git a/pkg/bluetooth/bluetooth_test.go b/pkg/bluetooth/bluetooth_test.go new file mode 100644 index 0000000..3bf1183 --- /dev/null +++ b/pkg/bluetooth/bluetooth_test.go @@ -0,0 +1,106 @@ +package bluetooth + +import ( + "bytes" + "sync" + "testing" + "time" + + "github.com/avereha/pod/pkg/message" +) + +// wire delivers one side's notifications as the other side's writes, or +// drops them while it has no peer. +type wire struct { + mu sync.Mutex + peer *Ble +} + +func (w *wire) to() *Ble { + w.mu.Lock() + defer w.mu.Unlock() + return w.peer +} + +func (w *wire) NotifyCmd(p Packet) error { + if b := w.to(); b != nil { + b.CmdWritten(p) + } + return nil +} + +func (w *wire) NotifyData(p Packet) error { + if b := w.to(); b != nil { + b.DataWritten(p) + } + return nil +} + +func (w *wire) Advertise([]byte) error { return nil } +func (w *wire) AdvertiseState(AdvertState) error { return nil } +func (w *wire) Disconnect() {} + +func newEnd(t *testing.T, w *wire) *Ble { + b, err := New(func(*Ble) (Radio, error) { return w, nil }) + if err != nil { + t.Fatal(err) + } + return b +} + +// The pod waits for a phone's first command before the phone connects. +func TestReadCmdFollowsANewConnection(t *testing.T) { + pod := newEnd(t, &wire{}) + got := make(chan Packet, 1) + go func() { + p, _ := pod.ReadCmd() + got <- p + }() + time.Sleep(50 * time.Millisecond) + pod.Connected() + pod.CmdWritten(CmdRTS) + select { + case p := <-got: + if !bytes.Equal(p, CmdRTS) { + t.Fatalf("read %x", p) + } + case <-time.After(time.Second): + t.Fatal("the first command of a new connection went unread") + } +} + +// A message the phone abandoned when its connection ended leaves no part of +// itself in the pod's reading of the next connection's messages. +func TestANewConnectionReadsOnlyItsOwnPackets(t *testing.T) { + toPhone, toPod := &wire{}, &wire{} + pod, phone := newEnd(t, toPhone), newEnd(t, toPod) + toPod.peer = pod + + // The phone starts a message, the pod clears it to send, and the + // connection ends with one packet of it written. + pod.Connected() + pod.StartMessageLoop() + pod.CmdWritten(CmdRTS) + time.Sleep(50 * time.Millisecond) + pod.DataWritten(append(Packet{0, 3, 0x54, 0x57, 0x10, 0x02}, make([]byte, 14)...)) + time.Sleep(50 * time.Millisecond) + + pod.Connected() + pod.StartMessageLoop() + toPhone.mu.Lock() + toPhone.peer = phone + toPhone.mu.Unlock() + phone.Connected() + phone.StartMessageLoop() + + sent := message.NewMessage(message.MessageTypeClear, []byte{1, 2, 3, 4}, []byte{5, 6, 7, 8}) + sent.Payload = bytes.Repeat([]byte{0xA5}, 40) + phone.WriteMessage(sent) + got, timedOut, _ := pod.ReadMessageWithTimeout(2 * time.Second) + if timedOut { + t.Fatal("the pod did not read the new connection's message") + } + if !bytes.Equal(got.Payload, sent.Payload) { + t.Fatalf("read payload % x, want % x", got.Payload, sent.Payload) + } +} From cb1d4cbc6022508cd1bdae45ce36fb2169313d08 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Mon, 12 Oct 2026 12:08:01 -0500 Subject: [PATCH 4/4] Note that Loop treats a pod fault as final OmnipodKit quiets its C00A fault scan once it detects a fault and only lifts that when alerts clear or it pairs a new pod. A fault cleared from the control panel leaves the scan off, so a second fault test needs a relaunched Loop. --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e1454ea..6ed05a2 100644 --- a/README.md +++ b/README.md @@ -105,7 +105,8 @@ does, and hangs up after 3 minutes without a command, as above. Its advertisement shows a fault or a firing alert, as a real pod's does: the 2nd service UUID becomes `C00A` on a fault (`C005` for an alert), and the manufacturer data carries the fault code and alert slots. A fault set from the control panel reaches a backgrounded Loop the way a real one does, through its scan for `C00A`; with `impossible-mux -suspend` -the advertisement wakes the suspended app. +the advertisement wakes the suspended app. A real pod's fault is final, and Loop treats it so: clearing the fault here +doesn't restart Loop's scan for `C00A`. Relaunch Loop before testing another fault. # Original README.md