diff --git a/README.md b/README.md index cb2eec7..6ed05a2 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,12 @@ 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. 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 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 8e6c6a7..db1b6a2 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, @@ -58,9 +93,22 @@ func AdvertisedServices(podId []byte) []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 @@ -82,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), @@ -116,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() @@ -152,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 @@ -164,6 +224,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 @@ -182,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 { @@ -237,19 +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 { - 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 + } + select { + case b.messageInput <- msg: + case <-stop: + return } - b.messageInput <- msg } } } @@ -266,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() { @@ -277,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) @@ -318,7 +415,7 @@ func (b *Ble) writeMessage(msg *message.Message) { buf.Write(bytes[14:]) b.writeDataBuffer(&buf) } - return + return nil } size := len(bytes) @@ -362,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 @@ -377,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 @@ -394,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 { @@ -406,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 @@ -417,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) + } +} 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.