From 9f7d872f03ab71eb1f64058f23b53feeebcad7f2 Mon Sep 17 00:00:00 2001 From: Pete Schwamb Date: Wed, 4 Nov 2026 16:16:25 -0600 Subject: [PATCH] Stop a bolus at a fault and keep reporting what it left undelivered A faulted pod's bolus remaining was counted down to the time the status was read, so a fault read after the bolus's scheduled end reported nothing undelivered, and one read earlier reported less than was left at the fault. A real pod stops delivering at the fault. SetFault now saves the bolus remaining at the fault, takes it back off the delivered total, and ends the bolus. A faulted pod reports the saved amount; clearing the fault forgets it. --- README.md | 2 +- pkg/pod/pod.go | 3 +-- pkg/pod/state.go | 23 +++++++++++++++++ pkg/pod/state_test.go | 57 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 pkg/pod/state_test.go diff --git a/README.md b/README.md index b0e4322..046921b 100644 --- a/README.md +++ b/README.md @@ -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://: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, and `crashNextCommand` with `"beforeProcessing": true|false`). +* It serves a control panel at http://: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, and `crashNextCommand` with `"beforeProcessing": true|false`). 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. diff --git a/pkg/pod/pod.go b/pkg/pod/pod.go index 22add65..c5b2abc 100644 --- a/pkg/pod/pod.go +++ b/pkg/pod/pod.go @@ -820,8 +820,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() diff --git a/pkg/pod/state.go b/pkg/pod/state.go index 2b3f7a4..4b7756d 100644 --- a/pkg/pod/state.go +++ b/pkg/pod/state.go @@ -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 @@ -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) { diff --git a/pkg/pod/state_test.go b/pkg/pod/state_test.go new file mode 100644 index 0000000..ca88874 --- /dev/null +++ b/pkg/pod/state_test.go @@ -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) + } +}