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, 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. A reset fault (0x0D, 0x0E, 0x0F, 0x10, 0x12, 0x22, 0x34, 0x3B, 0xAA, 0xD6 or 0xD7) reports what a reset pod does instead: no fault time, nothing delivered or left undelivered, and minutes active counted from the reset.

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

Expand Down
9 changes: 5 additions & 4 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ func (p *Pod) makeGeneralStatusResponse() response.Response {
func (p *Pod) makeDetailedStatusResponse() response.Response {

var now = time.Now()
delivered, bolusRemaining := p.state.ReportedDelivery()

return &response.DetailedStatusResponse{
LastProgSeqNum: p.state.LastProgSeqNum,
Expand All @@ -608,9 +609,9 @@ func (p *Pod) makeDetailedStatusResponse() response.Response {
BasalActive: p.state.BasalActive,
ExtendedBolusActive: p.state.ExtendedBolusActive,
PodProgress: p.state.PodProgress,
Delivered: p.state.Delivered,
BolusRemaining: p.state.BolusRemaining(),
MinutesActive: p.state.MinutesActive(),
Delivered: delivered,
BolusRemaining: bolusRemaining,
MinutesActive: p.state.ReportedMinutesActive(),
FaultEvent: p.state.FaultEvent,
FaultEventTime: p.state.FaultTime,
}
Expand All @@ -628,7 +629,7 @@ func (p *Pod) makeType3StatusResponse() response.Response {
return &response.Type3StatusResponse{
FaultEvent: p.state.FaultEvent,
FaultEventTime: p.state.FaultTime,
MinutesActive: p.state.MinutesActive(),
MinutesActive: p.state.ReportedMinutesActive(),
}
}

Expand Down
39 changes: 39 additions & 0 deletions pkg/pod/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ type PODState struct {
// pod keeps reporting it, however late it is read.
FaultBolusRemaining uint16 `toml:"fault_bolus_remaining"`

// ResetAt is when a reset fault restarted the pod's counters.
ResetAt time.Time `toml:"reset_at"`

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

// At some point these could be replaced with details
Expand Down Expand Up @@ -134,6 +137,42 @@ func (p *PODState) SetFault(code uint8) {
}
p.FaultEvent = code
p.FaultTime = p.MinutesActive()
if IsResetFault(code) {
if p.ResetAt.IsZero() {
p.ResetAt = time.Now()
}
p.FaultTime = 0
} else {
p.ResetAt = time.Time{}
}
}

// IsResetFault is a fault that resets the pod. Its status then holds no
// fault time, nothing delivered or left undelivered, and minutes counted
// from the reset, as every reset in field reports does.
func IsResetFault(code uint8) bool {
switch code {
case 0x0D, 0x0E, 0x0F, 0x10, 0x12, 0x22, 0x34, 0x3B, 0xAA, 0xD6, 0xD7:
return true
}
return false
}

// ReportedMinutesActive is the pod's minute counter, which a reset restarts.
func (p *PODState) ReportedMinutesActive() uint16 {
if !p.ResetAt.IsZero() {
return uint16(time.Now().Sub(p.ResetAt).Round(time.Minute).Minutes())
}
return p.MinutesActive()
}

// ReportedDelivery is the delivered total and bolus remaining the pod
// reports, both of which a reset clears.
func (p *PODState) ReportedDelivery() (delivered uint16, bolusRemaining uint16) {
if !p.ResetAt.IsZero() {
return 0, 0
}
return p.Delivered, p.BolusRemaining()
}

// NOTE: only handles immediate boluses; any extended bolus is not accounted for
Expand Down
53 changes: 53 additions & 0 deletions pkg/pod/state_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package pod

import (
"encoding/hex"
"testing"
"time"

Expand Down Expand Up @@ -55,3 +56,55 @@ func TestAFaultWithNoBolusLeavesNothingUndelivered(t *testing.T) {
t.Fatalf("remaining %d, delivered %d; want 0 and 1080", got, s.Delivered)
}
}

func TestAResetFaultClearsWhatThePodReports(t *testing.T) {
// 3 U (60 pulses) bolus, 20 s in, on a pod 18 minutes old
s := &PODState{
PodProgress: response.PodProgressRunningAbove50U,
ActivationTime: time.Now().Add(-18 * time.Minute),
Delivered: 149,
Reservoir: 2851,
BolusEnd: time.Now().Add(100 * time.Second),
}
s.SetFault(0x10)

p := &Pod{state: s}
raw, _ := p.makeDetailedStatusResponse().Marshal()
got := hex.EncodeToString(raw)
// no bolus remaining, nothing delivered, fault 0x10 at no time, 0 minutes active
want := "0216020d00000000000010000003ff0000"
if got[:len(want)] != want {
t.Fatalf("reset fault status %s, want it to start %s", got, want)
}

// The pod's own books still have the bolus stopped at the fault
if s.Delivered != 99 || s.FaultBolusRemaining != 50 {
t.Fatalf("delivered %d, fault bolus remaining %d; want 99 and 50", s.Delivered, s.FaultBolusRemaining)
}

s.ResetAt = time.Now().Add(-3 * time.Minute)
if got := s.ReportedMinutesActive(); got != 3 {
t.Fatalf("minutes active 3 minutes after the reset = %d, want 3", got)
}

s.SetFault(0)
if !s.ResetAt.IsZero() || s.ReportedMinutesActive() != 18 {
t.Fatalf("clearing the fault left reset at %v, minutes active %d", s.ResetAt, s.ReportedMinutesActive())
}
}

func TestANonResetFaultKeepsItsTimeAndCounters(t *testing.T) {
s := &PODState{
PodProgress: response.PodProgressRunningAbove50U,
ActivationTime: time.Now().Add(-100 * time.Minute),
Delivered: 1080,
}
s.SetFault(0x18)

if s.FaultTime != 100 || !s.ResetAt.IsZero() {
t.Fatalf("fault time %d, reset at %v; want 100 and none", s.FaultTime, s.ResetAt)
}
if delivered, _ := s.ReportedDelivery(); delivered != 1080 || s.ReportedMinutesActive() != 100 {
t.Fatalf("reported delivered %d, minutes %d; want 1080 and 100", delivered, s.ReportedMinutesActive())
}
}
Loading