Skip to content
Merged
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).
* 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.

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

Expand Down
3 changes: 1 addition & 2 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,8 +832,7 @@ func (p *Pod) SetAlerts(newVal uint8) {

func (p *Pod) SetFault(newVal uint8) {
p.mtx.Lock()
p.state.FaultEvent = newVal
p.state.FaultTime = p.state.MinutesActive()
p.state.SetFault(newVal)
p.state.Save()
p.takeSnapshot()
p.mtx.Unlock()
Expand Down
23 changes: 23 additions & 0 deletions pkg/pod/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ type PODState struct {
FaultTime uint16 `toml:"fault_time"`
Delivered uint16 `toml:"delivered"`

// FaultBolusRemaining is the bolus a fault left undelivered. A faulted
// pod keeps reporting it, however late it is read.
FaultBolusRemaining uint16 `toml:"fault_bolus_remaining"`

TriggerTimes [8]uint16 `toml:"trigger_times"`

// At some point these could be replaced with details
Expand Down Expand Up @@ -116,8 +120,27 @@ func (p *PODState) MinutesActive() uint16 {
return uint16(time.Now().Sub(p.ActivationTime).Round(time.Minute).Minutes())
}

// SetFault stops the bolus where it is, so what it left undelivered is not
// counted as delivered. Clearing the fault (0) forgets it.
func (p *PODState) SetFault(code uint8) {
if code == 0 {
p.FaultBolusRemaining = 0
} else if p.FaultEvent == 0 {
remaining := p.BolusRemaining()
p.FaultBolusRemaining = remaining
p.Delivered -= remaining
p.Reservoir += remaining
p.BolusEnd = time.Time{}
}
p.FaultEvent = code
p.FaultTime = p.MinutesActive()
}

// NOTE: only handles immediate boluses; any extended bolus is not accounted for
func (p *PODState) BolusRemaining() uint16 {
if p.FaultEvent != 0 {
return p.FaultBolusRemaining
}
now := time.Now()
var secondsPerPulse uint16
if p.BolusEnd.After(now) {
Expand Down
57 changes: 57 additions & 0 deletions pkg/pod/state_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package pod

import (
"testing"
"time"

"github.com/avereha/pod/pkg/response"
)

func TestAFaultFreezesTheBolusRemaining(t *testing.T) {
// An 80 pulse (4 U) bolus, 20 s in: 70 pulses still to go
s := &PODState{
PodProgress: response.PodProgressRunningAbove50U,
ActivationTime: time.Now().Add(-2 * time.Hour),
Delivered: 1080,
Reservoir: 920,
BolusEnd: time.Now().Add(140 * time.Second),
}

s.SetFault(0x18)

if got := s.BolusRemaining(); got != 70 {
t.Fatalf("bolus remaining after the fault = %d, want 70", got)
}
if s.Delivered != 1010 || s.Reservoir != 990 {
t.Fatalf("delivered %d, reservoir %d; want 1010 and 990", s.Delivered, s.Reservoir)
}
if !s.BolusEnd.IsZero() {
t.Fatalf("the bolus is still running after the fault, until %v", s.BolusEnd)
}

// A second fault code doesn't take the remainder again
s.SetFault(0x14)
if got := s.BolusRemaining(); got != 70 || s.Delivered != 1010 {
t.Fatalf("after a second fault: remaining %d, delivered %d; want 70 and 1010", got, s.Delivered)
}

s.SetFault(0)
if got := s.BolusRemaining(); got != 0 {
t.Fatalf("bolus remaining after clearing the fault = %d, want 0", got)
}
}

func TestAFaultWithNoBolusLeavesNothingUndelivered(t *testing.T) {
s := &PODState{
PodProgress: response.PodProgressRunningAbove50U,
ActivationTime: time.Now().Add(-2 * time.Hour),
Delivered: 1080,
BolusEnd: time.Now().Add(-time.Minute),
}

s.SetFault(0x18)

if got := s.BolusRemaining(); got != 0 || s.Delivered != 1080 {
t.Fatalf("remaining %d, delivered %d; want 0 and 1080", got, s.Delivered)
}
}
Loading