BUMarjorie Algorithm Loop Scaffolding (Legacy App)
This document summarizes how the legacy BUMarjorie app orchestrates its algorithm loop, inputs, outputs, and state. It focuses on app scaffolding and process flow rather than algorithm math.
Scope and References
Primary implementation sources reviewed:
- App entry + UI:
Marjorie/MAppDelegate.m,Marjorie/MViewController.m,Marjorie/MSettingsViewController.m,Marjorie/MAdvancedSettingsViewController.m,Marjorie/MOfflineInsulinViewController.m - Controllers:
ControllerFramework/Controllers/MasterController.m,ControllerFramework/Controllers/AlgorithmController.m,ControllerFramework/Controllers/StateController.m,ControllerFramework/Controllers/CGMController.m,ControllerFramework/Controllers/PumpController.m,ControllerFramework/Controllers/DeviceController.m,ControllerFramework/Controllers/GCDController.m,ControllerFramework/Controllers/ThreadedController.m - Persistence + model:
ControllerFramework/Models/AlgorithmStep.m,Marjorie/Data/MStepDataModel.m - Sync/export:
ControllerFramework/Controllers/APIController.m
High-Level Architecture
- MasterController is the orchestrator. It owns the algorithm controller, CGM controller, pump controllers, state controller, and (optionally) watchdog and API controller.
- StateController persists device/session state, algorithm time, and settings in UserDefaults plus algorithm state data in a binary file.
- AlgorithmController builds step inputs from pump/CGM/state, calls the algorithm, delivers insulin/glucagon, persists state, and logs AlgorithmStep records.
- CGMController / PumpController handle device connectivity, communication, and status caching.
- UI (MViewController / Settings) subscribes to notifications and drives user actions (start session, toggle open-loop, enter BG, record meal/bolus, configure settings).
- MStepDataModel and chart views render the historical data stored in Core Data (AlgorithmStep).
Orchestration and Scheduling
MasterController startup
- App delegate initializes
MasterControllerand callsstart(MAppDelegate.m). MasterControllerinitializes defaults, controllers (CGM, pumps, algorithm), and starts device connections (MasterController.m).- Optional background keep-alive uses silent audio + worker thread if
audioBGModeis enabled.
Work scheduling loop
MasterControllerruns on a worker thread (ThreadedController) if background mode is enabled; otherwise it relies on scheduled events (GCDController).- Each cycle calls
doWork: - Computes time until the next step based on
firstStepTimeandalgorithmTime. - Schedules device reads and algorithm runs about every 5 minutes (with a 27-second early window) when a session is active.
- If no session is active, it still polls CGM and pumps on a slower cadence and pings the algorithm so the watchdog doesn’t trigger.
DeviceControlleruses a retry loop and schedules follow-up work after communication errors (10s backoff).
Relevant code:
MasterController.m: doWorkWithTimeBeforeStep, timeUntilNextStep, doWorkThreadedController.m,GCDController.m,DeviceController.m
Algorithm Step Lifecycle
Input assembly
AlgorithmController constructs AlgorithmStepInputData with:
- Timing / session:
algorithmTime,firstStepTime,lastWorldTime,lastCgmTime - CGM value: from
CGMController.lastValue, with guardrails - Ignore CGM if not newer than last used timestamp
- Ignore CGM if older than 15 minutes
- Pump status: availability and last bolus status from insulin and glucagon pump controllers
- Reads and clears completed bolus status
- Marks pump unavailable if status not READY or bolus read fails
- User inputs:
subjectId,subjectWeightbgValue,bgCal,bolusAmountmealTime,mealSizesetPointNominal,setPoint(target)basalRateMultiplier(temp basal)- Custom input fields
- Algorithm state: serialized
stateDatafrom disk
Relevant code: AlgorithmController.m: prepareInputData*, StateController.m
Guard condition on first step
The first step will not run unless a CGM (or BG) value exists:
- If
algorithmTime == 0and both CGM and BG are missing, the algorithm step is skipped until a valid value arrives.
Relevant code: AlgorithmController.m: step
Algorithm execution
AlgorithmControllercallsalgorithmRunStep(input, output)or a delegate override.- The output includes requested insulin/glucagon, delivery metadata, open-loop mode, and algorithm state data buffer.
Delivery and state update
After running the algorithm:
- Deliver fluid:
- Computes total insulin (basal + bolus + meal) and glucagon requested.
- If pump is available, calls
requestBolus(units, id)withalgorithmTimeas the ID. - If pump is unavailable, logs and skips delivery.
- Persist state:
- Writes algorithm state buffer to disk (
statedata.bin). - Updates
lastCgmTime,lastWorldTime, and incrementsalgorithmTime. - Sets
firstStepTimeon the first successful step. - Cancels temporary basal when re-entering closed loop.
- Reset transient inputs (meal, bolus, BG, etc.) for the next cycle.
- Save AlgorithmStep to Core Data and fire notification.
Relevant code: AlgorithmController.m: deliverFluid, saveState, prepareForNextStep, saveAlgorithmStepAndFireNotification
Notification and UI Flow
Core notifications
DeviceStateChangedNotification: fired by device controllers when CGM/pump state changes.AlgorithmStepNotification: fired after each saved step.- Additional events:
BGCheckNotification,MealBolusNotification,DevicePairedNotification,WatchdogNotification.
Main view updates (MViewController)
- Observes device and algorithm notifications to update:
- CGM value and timestamp display (red text if stale or missing)
- Pump status indicators
- Loop status indicator (open/closed loop and session state)
- Charts and daily summary (24h insulin, glucagon, min/max/avg glucose)
- Uses CGM data age guard with a 310-second timer to show stale status if no new reading is received.
Relevant code: MViewController.m: deviceStateChanged, algorithmStep, updateLoopStatus, refresh
Session and loop control
- Settings and main UI allow:
- Starting a new session (
MasterController startNewSession) - Toggling open-loop vs closed-loop (
forceOpenLoopMode) - Loop status is derived from both
forceOpenLoopModeandlastStepOutput.openLoopMode.
Relevant code: MSettingsViewController.m, MViewController.m
Manual inputs
- BG check: user-entered BG saved to
AlgorithmController.bgValue. - Meal bolus: user requests bolus amount and meal timing; input is posted via
AlgorithmController. - Temporary basal: offline basal UI sets
basalRateMultiplierand steps; tracked with a countdown timer.
Relevant code: MAdvancedSettingsViewController.m, MSettingsViewController.m, MOfflineInsulinViewController.m
Persistence and State Recovery
Persistent values (UserDefaults)
- Session state:
SessionInProgress,FirstStepTime,AlgorithmTime,LastCgmTime,LastWorldTime - User config:
SubjectID,SubjectWeight,SubjectTransmitterID,SetPointNominal, temporary setpoint, temp basal multiplier/steps - Pump pairing:
PairedInsulinPumpId,PairedGlucagonPumpId - Open-loop mode:
ForceOpenLoopMode - Custom input fields
State data on disk
- Algorithm state buffer is written to
Documents/statedata.bineach step and reloaded when needed.
Relevant code: StateController.m: saveStateData, stateData
Core Data
- Every step produces an
AlgorithmSteprecord with full input/output metadata, used by: - UI charts
- data export
- remote API sync (SweetSpot)
Relevant code: StateController.m: saveAlgorithmStepWithInputData
Data Export and Sync
APIControllersubscribes toAlgorithmStepNotificationand can transmit steps to a remote endpoint.- Export uses AlgorithmStep fields with extra metadata (battery state, CGM comm errors, custom fields).
Relevant code: APIController.m, MAdvancedSettingsViewController.m
Sequence Diagram (Standard Step)
sequenceDiagram
participant UI as MViewController
participant MC as MasterController
participant CGM as CGMController
participant PumpI as PumpController(Insulin)
participant PumpG as PumpController(Glucagon)
participant AC as AlgorithmController
participant SC as StateController
participant DB as Core Data
Note over MC: Worker loop (ThreadedController) or GCD scheduled
MC->>CGM: scheduleWorkNow()
MC->>PumpI: scheduleWorkNow()
MC->>PumpG: scheduleWorkNow()
MC->>AC: scheduleWorkNow()
CGM-->>MC: DeviceStateChangedNotification
PumpI-->>MC: DeviceStateChangedNotification
PumpG-->>MC: DeviceStateChangedNotification
AC->>SC: read settings + stateData + algorithmTime
AC->>CGM: lastValue (guarded for freshness)
AC->>PumpI: readAndClearCompletedBolusStatus
AC->>PumpG: readAndClearCompletedBolusStatus
AC->>AC: runAlgorithmStep(input, output)
AC->>PumpI: requestBolus(units, algorithmTime)
AC->>PumpG: requestBolus(units, algorithmTime)
AC->>SC: saveStateData + update timestamps + increment algorithmTime
AC->>DB: save AlgorithmStep
AC-->>UI: AlgorithmStepNotification
UI->>UI: refresh indicators, charts, and status
Sequence Diagram (Alternate Paths)
sequenceDiagram
participant UI as MViewController
participant MC as MasterController
participant CGM as CGMController
participant PumpI as PumpController(Insulin)
participant PumpG as PumpController(Glucagon)
participant AC as AlgorithmController
participant SC as StateController
alt First step with no CGM or BG
AC->>CGM: lastValue
AC->>AC: detect CGM/BG missing on step 0
AC-->>UI: no AlgorithmStepNotification (step skipped)
else Stale CGM reading
AC->>CGM: lastValue
AC->>AC: reject CGM if <= lastCgmTime or >15 minutes old
AC->>AC: run algorithm with CGM_VALUE_NONE
end
alt Pump not ready or unavailable
AC->>PumpI: readAndClearCompletedBolusStatus
AC->>PumpG: readAndClearCompletedBolusStatus
AC->>AC: mark pump unavailable if status != READY or read fails
AC->>AC: log and skip delivery for unavailable pump
end
alt Open-loop mode forced
UI->>AC: set forceOpenLoopMode = TRUE
AC->>AC: set inputData.openLoopMode = TRUE
AC->>SC: save state; temp basal may persist
end
Sequence Diagram (Watchdog Freeze)
sequenceDiagram
participant UI as MViewController
participant WD as WatchdogController
participant MC as MasterController
WD-->>UI: WatchdogNotification (frozenControllers)
UI->>UI: updateLoopStatus() disables buttons
UI->>UI: show error indicator + alert
UI-->>MC: user cannot resume loop until issue clears
Sequence Diagram (Offline Temp Basal)
sequenceDiagram
participant UI as MOfflineInsulinViewController
participant AC as AlgorithmController
participant SC as StateController
participant Timer as NSTimer
UI->>AC: setTemporaryBasalRate(multiplier, steps)
UI->>SC: store expectedTempBasalEnd (UserDefaults)
UI->>Timer: start 1s countdown
loop while stepsRemaining > 0
Timer-->>UI: tick
UI->>AC: temporaryBasalRateStepsRemaining
UI->>UI: update countdown label
end
UI->>AC: setTemporaryBasalRate(1.0, 0)
UI->>SC: clear expectedTempBasalEnd
Sequence Diagram (Session Start/Stop)
sequenceDiagram
participant UI as MSettingsViewController
participant MC as MasterController
participant SC as StateController
participant AC as AlgorithmController
alt Start new session
UI->>MC: startNewSession()
MC->>SC: startNewSession() resets timers/state
MC->>AC: reset()
UI->>UI: update loop label/indicators
else End session
UI->>MC: endSession()
MC->>SC: endSession() clears state + deletes stateData.bin
UI->>UI: update loop label/indicators
end
Key Inputs and Outputs (as used by scaffolding)
Inputs captured or derived by the app
- CGM glucose value + timestamp
- Pump status, availability, and last bolus delivered
- Subject ID, weight
- Target set point (nominal) and temporary set point
- Basal rate multiplier (temporary basal)
- Meal size/time and manual bolus amount
- Optional BG check and BG calibration
- Custom input fields
- Saved algorithm state buffer
Outputs processed by the app
- Insulin and glucagon requested and delivered (bolus totals)
- Open-loop / closed-loop mode
- Delivery time, hypo projections, slope, and additional metrics
- State buffer for the next step
Pump Disconnect and Insulin-Unavailable Handling (Legacy Detail)
This section captures exactly how BUMarjorie handled insulin pump disconnects and unavailable states during normal loop execution.
Availability determination per step
- Availability starts from
insulinPumpController.isConnected. - For steps after step 0,
readAndClearCompletedBolusStatusis attempted. - If bolus-status read fails, insulin pump is marked unavailable for that step.
- Pump must report
PUMP_READY; otherwise it is marked unavailable for that step. - The same pattern exists for glucagon pump availability.
Implementation sources:
ControllerFramework/Controllers/AlgorithmController.m(prepareInputDataFromPumps)ControllerFramework/Controllers/PumpController.m(readAndClearCompletedBolusStatus)
What the algorithm receives when insulin is unavailable
inputData.insulinPumpStatus.available = FALSEinputData.insulinPumpStatus.requestTime = INVALID_REQUEST_TIMEinputData.insulinPumpStatus.unitsRequested = INVALID_FLUID_AMOUNTinputData.insulinPumpStatus.unitsDelivered = INVALID_FLUID_AMOUNT
That means the algorithm still runs, but it is explicitly informed that insulin pump status/feedback is unavailable for that step.
Delivery behavior when algorithm requests insulin but pump is unavailable
- Algorithm output is still computed for the step.
- If insulin is requested and pump is unavailable:
- No bolus is sent.
- A log entry is emitted (
Algorithm requested insulin bolus when pump is unavailable). - If pump is available, bolus is requested using
algorithmTimeas request ID.
Implementation source:
ControllerFramework/Controllers/AlgorithmController.m(deliverFluid)
Step/state behavior during unavailable pump conditions
- Step still persists (
saveStateData, step record written). algorithmTimeis incremented even if no insulin could be delivered.firstStepTimeis set at step 0 once first step completes.
Implementation source:
ControllerFramework/Controllers/AlgorithmController.m(saveState,step)
Disconnect/reconnect processing pattern
- Device communications retry up to configured attempts.
- Communication failures can force disconnect state.
- Controller re-schedules work after 10 seconds on communication/connection failures.
DeviceStateChangedNotificationtriggers immediate controller wake (scheduleWorkNow).- Master scheduler continuously re-schedules device reads + algorithm scheduling windows each cycle.
Implementation sources:
ControllerFramework/Controllers/DeviceController.mControllerFramework/Controllers/MasterController.m(doWorkWithTimeBeforeStep,doWork)
Practical takeaway for BionicLoop policy
BUMarjorie used a "run-step-with-unavailable-input" model, not a hard skip, when pump comms were degraded:
- Step executes and advances.
- Algorithm sees unavailable pump input fields.
- Delivery can be skipped safely when pump unavailable.
- Reconnect then restores feedback path on subsequent steps.
Takeaways for BionicLoop Integration
- The legacy loop is driven by a central scheduler (MasterController) that keeps devices and algorithm steps in lock-step on a fixed cadence.
- Input assembly is defensive: it filters stale CGM values, validates pump readiness, and records pump-delivery status per step.
- State continuity relies on three layers:
- UserDefaults for scalar session/config values
- Disk file for algorithm state buffer
- Core Data for per-step telemetry
- UI is notification-driven, with algorithm steps and device state changes flowing back into the UI via
NSNotificationCenter.
Replication Needs for the New App (No Code Yet)
These are the app-level behaviors to replicate for BionicLoop, focusing on UI and algorithm coordination. Glucagon delivery is not used, so glucagon availability should always be false/0 in algorithm inputs and telemetry.
Algorithm Coordination
- Central scheduler to align device polling and algorithm step cadence (5-minute loop) with an early-read window.
- Guardrails for CGM freshness (skip stale or duplicate readings; allow BG fallback for step 0).
- Pump readiness checks before delivery; capture delivery status per step.
- Persist algorithm state buffer between launches and apply it to each step.
- Maintain
algorithmTimeandfirstStepTimeto prevent drift; handle step skipping when late.
Session and State Management
- Explicit session start/stop with state resets and data retention rules.
- Persist user configuration (subject ID, weight, target), device pairing, open-loop flag.
- Record every step into a durable log (for UI, export, and troubleshooting).
UI and UX
- Home screen indicators for loop status, CGM freshness, and pump availability.
- Settings controls for starting/stopping loop, target adjustments, subject info.
- Manual entry paths (BG checks, meal bolus, temporary basal/offline adjustments).
- Clear messaging for stale CGM or unavailable pump conditions.
Glucagon Handling
- Always set glucagon pump availability to false in inputs.
- Ignore glucagon delivery outputs and do not display or attempt glucagon dosing.
Virtual Patient and Intended Use (Legacy Notes)
The legacy code uses a VirtualPatient to exercise the loop without real hardware. The ControllerFramework changelog explicitly notes VirtualPatient fixes (e.g., fluid type initialization and pump delivery behavior), which implies its intended use is for simulation and algorithm testing while maintaining the same controller scheduling and delivery flow.
For BionicLoop, we should add a similar simulation mode to drive the algorithm with deterministic CGM inputs and virtual pump responses so we can test loop coordination, persistence, and UI behavior without real devices.