Skip to content

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 MasterController and calls start (MAppDelegate.m).
  • MasterController initializes defaults, controllers (CGM, pumps, algorithm), and starts device connections (MasterController.m).
  • Optional background keep-alive uses silent audio + worker thread if audioBGMode is enabled.

Work scheduling loop

  • MasterController runs 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 firstStepTime and algorithmTime.
  • 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.
  • DeviceController uses a retry loop and schedules follow-up work after communication errors (10s backoff).

Relevant code:

  • MasterController.m: doWorkWithTimeBeforeStep, timeUntilNextStep, doWork
  • ThreadedController.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, subjectWeight
  • bgValue, bgCal, bolusAmount
  • mealTime, mealSize
  • setPointNominal, setPoint (target)
  • basalRateMultiplier (temp basal)
  • Custom input fields
  • Algorithm state: serialized stateData from 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 == 0 and both CGM and BG are missing, the algorithm step is skipped until a valid value arrives.

Relevant code: AlgorithmController.m: step

Algorithm execution

  • AlgorithmController calls algorithmRunStep(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) with algorithmTime as the ID.
  • If pump is unavailable, logs and skips delivery.
  • Persist state:
  • Writes algorithm state buffer to disk (statedata.bin).
  • Updates lastCgmTime, lastWorldTime, and increments algorithmTime.
  • Sets firstStepTime on 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 forceOpenLoopMode and lastStepOutput.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 basalRateMultiplier and 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.bin each step and reloaded when needed.

Relevant code: StateController.m: saveStateData, stateData

Core Data

  • Every step produces an AlgorithmStep record with full input/output metadata, used by:
  • UI charts
  • data export
  • remote API sync (SweetSpot)

Relevant code: StateController.m: saveAlgorithmStepWithInputData

Data Export and Sync

  • APIController subscribes to AlgorithmStepNotification and 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, readAndClearCompletedBolusStatus is 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 = FALSE
  • inputData.insulinPumpStatus.requestTime = INVALID_REQUEST_TIME
  • inputData.insulinPumpStatus.unitsRequested = INVALID_FLUID_AMOUNT
  • inputData.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 algorithmTime as request ID.

Implementation source:

  • ControllerFramework/Controllers/AlgorithmController.m (deliverFluid)

Step/state behavior during unavailable pump conditions

  • Step still persists (saveStateData, step record written).
  • algorithmTime is incremented even if no insulin could be delivered.
  • firstStepTime is 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.
  • DeviceStateChangedNotification triggers immediate controller wake (scheduleWorkNow).
  • Master scheduler continuously re-schedules device reads + algorithm scheduling windows each cycle.

Implementation sources:

  • ControllerFramework/Controllers/DeviceController.m
  • ControllerFramework/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 algorithmTime and firstStepTime to 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.