Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Fake pod implementation

* This version also mimics a behavior we see in some DASH pods where the pod disconnects every 3 minutes; this can be used with iOS hooks to make a heartbeat to run Loop in situations where a BLE CGM is not available.

* It serves a control panel at http://<host>:8080/ (`-api` changes the address): the pod's reservoir, delivery, age, alerts and fault, with controls to set the reservoir and pod age, raise alerts, trigger or clear a fault, and crash on the next command to test unacknowledged commands. The panel drives a JSON API: `GET /status`, and `POST /command` with `{"command": "changeReservoir", "value": 45}` (also `setActiveTime` in minutes, `setAlerts` as a slot mask, `setFault` with a fault code, 0 to clear, `crashNextCommand` with `"beforeProcessing": true|false`, `ignoreNextMessages` with a count, which keeps the link up but leaves that many messages unanswered, as a pod that didn't hear them would, and `failNextConnects` with a count and an optional `hangUpAfter` in seconds, 7 by default, which leaves service discovery unanswered on that many connects and then hangs up with CoreBluetooth's "peripheral disconnected" error, as some pods do in the field; this needs `-transport impossible` and an ImpossiBLE that passes `errorDomain` and `errorCode` through on a disconnect). A fault stops a bolus in progress, and the pod reports the part left undelivered however late it is read, as a real pod does.
* It serves a control panel at http://<host>:8080/ (`-api` changes the address): the pod's reservoir, delivery, age, alerts and fault, with controls to set the reservoir and pod age, raise alerts, trigger or clear a fault, and crash on the next command to test unacknowledged commands. The panel drives a JSON API: `GET /status`, and `POST /command` with `{"command": "changeReservoir", "value": 45}` (also `setActiveTime` in minutes, `setAlerts` as a slot mask, `setFault` with a fault code, 0 to clear, `crashNextCommand` with `"beforeProcessing": true|false`, `ignoreNextMessages` with a count, which keeps the link up but leaves that many messages unanswered, as a pod that didn't hear them would, `hangUpOnNextMessages` with a count, which hangs up without answering when each of that many messages arrives, as a pod that drops the link during a command does, or with `"afterAnswering": true` once the phone has acknowledged the answer, and `failNextConnects` with a count and an optional `hangUpAfter` in seconds, 7 by default, which leaves service discovery unanswered on that many connects and then hangs up with CoreBluetooth's "peripheral disconnected" error, as some pods do in the field; this needs `-transport impossible` and an ImpossiBLE that passes `errorDomain` and `errorCode` through on a disconnect). A fault stops a bolus in progress, and the pod reports the part left undelivered however late it is read, as a real pod does.

* The websocket API at `/ws`, used by the separate [NodeJS/React frontend](https://github.com/ps2/pod_simulator_frontend), is still there.

Expand Down
6 changes: 6 additions & 0 deletions pkg/api/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,12 @@ func (s *Server) handleCommand(bytes []byte) error {
return err
}
s.pod.IgnoreNextMessages(int(value))
case "hangUpOnNextMessages":
if err := needValue("message count", 0, 100); err != nil {
return err
}
afterAnswering, _ := msg["afterAnswering"].(bool)
s.pod.HangUpOnNextMessages(int(value), afterAnswering)
case "crashNextCommand":
beforeProcessing, ok := msg["beforeProcessing"].(bool)
if !ok {
Expand Down
10 changes: 10 additions & 0 deletions pkg/bluetooth/bluetooth.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,16 @@ func (b *Ble) ShutdownConnection() {
b.radio.Disconnect()
}

// HangUp ends the connection the way a pod does on its own. On a Bluetooth
// adapter the phone already sees that as a remote close.
func (b *Ble) HangUp() {
if r, ok := b.radio.(interface{ HangUp() }); ok {
r.HangUp()
return
}
b.radio.Disconnect()
}

// MTUShortfall reports a link too small to carry a packet of this profile.
// The phone fragments a message into packets but does not fragment a packet,
// so a short link truncates every write silently, and pairing stalls on the
Expand Down
17 changes: 13 additions & 4 deletions pkg/bluetooth/impossible.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func (r *simRadio) connect(c *simClient) {
r.failConnects--
r.stalling = true
failing = true
r.hangUp = time.AfterFunc(r.hangUpAfter, func() { r.remoteHangUp(c) })
r.hangUp = time.AfterFunc(r.hangUpAfter, func() { r.remoteHangUp(c, "pod (failNextConnects)") })
}
}
r.mu.Unlock()
Expand Down Expand Up @@ -452,10 +452,19 @@ func (r *simRadio) isStalling() bool {
return r.stalling
}

// remoteHangUp ends a failing connection the way a pod does, with
// HangUp is the pod ending the connection on its own, with
// CBErrorPeripheralDisconnected as the phone sees a pod's hang-up.
func (r *simRadio) HangUp() {
r.mu.Lock()
c := r.client
r.mu.Unlock()
r.remoteHangUp(c, "pod")
}

// remoteHangUp ends a connection the way a pod does, with
// CBErrorPeripheralDisconnected.
func (r *simRadio) remoteHangUp(c *simClient) {
r.disconnectWithError(c, "pod (failNextConnects)", map[string]interface{}{
func (r *simRadio) remoteHangUp(c *simClient, by string) {
r.disconnectWithError(c, by, map[string]interface{}{
"error": "The specified device has disconnected from us.", "errorDomain": "CBErrorDomain", "errorCode": 7})
}

Expand Down
36 changes: 36 additions & 0 deletions pkg/bluetooth/impossible_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -299,3 +299,39 @@ func TestSimRadioFailsTheNextConnectAtServiceDiscovery(t *testing.T) {
app.send(map[string]interface{}{"type": "discoverServices", "id": id})
app.expect("didDiscoverServices")
}

func TestSimRadioHangsUpAsAPod(t *testing.T) {
dir, err := ioutil.TempDir("/tmp", "podsim")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(dir)
path := filepath.Join(dir, "impossible.sock")
const id = "11111111-2222-4333-8444-555555555555"
b, err := New(func(b *Ble) (Radio, error) { return NewSimRadio(b, path, id, nil) }, ProfileFor(ModeDash))
if err != nil {
t.Fatal(err)
}
conn, err := net.Dial("unix", path)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
app := &fakeApp{t: t, conn: conn, in: bufio.NewScanner(conn)}
app.send(map[string]interface{}{"type": "hello", "bundleId": "org.loopkit.Loop", "pid": 0})
app.send(map[string]interface{}{"type": "scan"})
app.expect("didDiscover")
app.send(map[string]interface{}{"type": "connect", "id": id})
app.expect("didConnect")

b.HangUp()
msg := app.expect("didDisconnect")
if msg["errorDomain"] != "CBErrorDomain" || msg["errorCode"] != float64(7) {
t.Fatalf("hang-up %v", msg)
}
if b.IsConnected() {
t.Fatal("still connected after hanging up")
}
app.send(map[string]interface{}{"type": "connect", "id": id})
app.expect("didConnect")
}
33 changes: 33 additions & 0 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Pod struct {
var crashBeforeProcessingCommand bool
var crashAfterProcessingCommand bool
var ignoreMessages int
var hangUpMessages int
var hangUpAfterAnswering bool

func New(ble *bluetooth.Ble, state *PODState) *Pod {
ret := &Pod{
Expand Down Expand Up @@ -350,6 +352,20 @@ func (p *Pod) CommandLoop(pMsg PodMsgBody) {
continue
}

p.mtx.Lock()
hangUp := hangUpMessages > 0
if hangUp {
hangUpMessages--
}
hangUpAfter := hangUp && hangUpAfterAnswering
p.mtx.Unlock()
if hangUp && !hangUpAfter {
log.Infof("pkg pod; hanging up on message seq %d without answering (hangUpOnNextMessages)", msg.SequenceNumber)
p.ble.HangUp()
go p.StartAcceptingCommands()
return
}

if msg.SequenceNumber == lastMsgSeq {
// this is a retry because we did not answer yet
// ignore duplicate commands/mesages
Expand Down Expand Up @@ -537,6 +553,12 @@ func (p *Pod) CommandLoop(pMsg PodMsgBody) {

log.Debugf("notifyingStateChange")
p.notifyStateChange()
if hangUpAfter {
log.Infof("pkg pod; hanging up after answering message seq %d (hangUpOnNextMessages)", lastMsgSeq)
p.ble.HangUp()
go p.StartAcceptingCommands()
return
}
}
}

Expand Down Expand Up @@ -854,6 +876,17 @@ func (p *Pod) IgnoreNextMessages(count int) {
p.mtx.Unlock()
}

// HangUpOnNextMessages makes the pod hang up on each of the next count
// messages: without answering, as a pod that drops the link during a
// command does, or once the phone has acknowledged the answer, as a pod
// that hangs up soon after a command does.
func (p *Pod) HangUpOnNextMessages(count int, afterAnswering bool) {
p.mtx.Lock()
hangUpMessages = count
hangUpAfterAnswering = afterAnswering
p.mtx.Unlock()
}

// FailNextConnects makes the next count connects fail at service
// discovery, with the pod hanging up after hangUpAfter.
func (p *Pod) FailNextConnects(count int, hangUpAfter time.Duration) error {
Expand Down
Loading