# GlanceStrain - Daily Strain Logger
## Problem Statement
Provide a glance-friendly strain summary that logs heart-rate-derived strain whenever sensor data arrives and the watch is on-hand. Strain is calculated from heart rate measurements and accumulated over time, with data persisted to daily FIT files.
## Logic Flow Diagram
```mermaid
flowchart TD
A[Kernel Starts App] --> B[Service::run]
B --> C{Event Type}
C -->|EVENT_GLANCE_START| D[Configure UI
Connect Sensors
Check Day Rollover]
C -->|EVENT_SENSOR_LAYER_DATA| E[onSdlNewData]
E --> R[checkDayRollover]
R --> Q{Which Sensor?}
Q -->|Touch| I[Update mIsOnHand]
Q -->|Activity| J[Update mActiveMin
Trigger FIT Write if On-Hand]
Q -->|HR| K{HR in 50..255 & On-Hand?}
K -->|Yes| L[Calculate Strain Delta
Accumulate Totals]
L --> J
C -->|EVENT_GLANCE_TICK| M[onGlanceTick
Update UI Display]
C -->|EVENT_GLANCE_STOP| N[Mark Glance Inactive
Keep Sensors Connected]
C -->|COMMAND_APP_STOP| O[Finalize FIT Session
Disconnect Sensors
Exit App]
J --> P[ActivityWriter::addRecord
Write FIT Record]
```
This diagram illustrates the core message-driven architecture of GlanceStrain, showing how sensor data flows through processing stages to accumulate strain metrics and persist them to FIT files.
## Program Architecture
### 1) Kernel entry and service lifetime
- `Service` is constructed with a reference to the kernel (`SDK::Kernel`) and runs as the glance service entry point.
- `Service::run()` is the main loop. It blocks on `mKernel.comm.getMessage()` and reacts to glance/tick/sensor messages. `EVENT_GLANCE_TICK` / `onGlanceTick()` only occur while the glance is active, and are used only for UI refresh.
- The service does **not** exit on `EVENT_GLANCE_STOP`; it keeps sensors connected and continues acquiring data in the background. Persistence is driven by sensor events, and background saves are allowed when the device is on-hand. The service exits only on `COMMAND_APP_STOP`.
### 2) Kernel message loop
`Service::run()` handles the following message types in order:
- `EVENT_GLANCE_START`
- Configure glance UI bounds (`configGui()`), build controls (`createGuiControls()`), mark `mGlanceActive = true`.
- Connect to sensors (`connect()`), handle day rollover (`checkDayRollover()`), and trigger an initial FIT save (`saveFit(true, false)`).
- `EVENT_GLANCE_TICK`
- Refresh display (`onGlanceTick()`). These ticks only occur while the glance is active; they do not fire in the background.
- `EVENT_SENSOR_LAYER_DATA`
- Dispatch batched sensor samples into `onSdlNewData()`.
- `EVENT_GLANCE_STOP`
- Mark the glance inactive and keep sensors connected; sensor-driven saves can continue when on-hand.
- `COMMAND_APP_STOP`
- Finalize the FIT session for the day, disconnect, and return from `run()`.
### 3) UI layer (Glance)
- `SDK::Glance::Form` (`mGlanceUI`) owns controls.
- `createGuiControls()` builds the UI layout:
- Icon image
- Title text ("Strain Score")
- Value text for current strain score
- `onGlanceTick()` updates the value text and issues a `RequestGlanceUpdate` when the UI is invalid; it only runs while the glance is active.
### 4) Sensor layer
- `SDK::Sensor::Connection` instances subscribe to:
- `HEART_RATE` → `SensorDataParserHeartRate`
- `ACTIVITY` → `SensorDataParserActivity`
- `TOUCH_DETECT` → `SensorDataParserTouch`
- All three sensor connections are configured with the same sample period (`skSamplePeriodSec = 5`, expressed in seconds, converted to ms).
- `onSdlNewData()` routes incoming batches by sensor handle:
- Touch updates `mIsOnHand`.
- Activity updates active minutes (`mActiveMin`) and triggers FIT record writes if on-hand.
- Heart rate updates strain accumulators and the most recent valid HR value.
- Sensor events enforce record cadence via the activity sensor connection interval, day rollover checks.
### 5) Core strain logic
- Samples are accepted only when HR is in the valid range [50, 255].
- Each HR sample contributes a normalized delta:
- `norm = (hr - 60.0f) / 120.0f`
- `delta = max(0.0f, norm) * 0.75f`
- Running aggregates:
- `mTotalStrain`, `mSumHR`, `mMaxHR`, `mSampleCount`, `mLastHr`
- On each activity sensor event, if the watch is on-hand, a pending FIT record is captured at the activity sensor cadence (5 seconds). This is independent of glance activity.
### 6) Persistence (FIT)
- A daily FIT file is created with name `strain_YYYY-MM-DD.fit` in the app filesystem.
- Records are written on each activity sensor event when on-hand (every 5 seconds).
- Session summary is written on day rollover or app stop.
- FIT file structure is built using `SDK::Component::FitHelper` definitions for file ID, developer data, events, records, sessions, and activity summaries. See [Docs/FitFiles-Structure.md](../../Docs/FitFiles-Structure.md) for detailed FIT file structure and ActivityWriter usage.
- The file header is rewritten after appends and CRC is recomputed on each save.
- No recovery of previous state; accumulators reset daily.
## Implementation Walktrought
1. **App layout**
- Service entry point and logic live in [`Software/Libs/Header/Service.hpp`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp) and [`Software/Libs/Source/Service.cpp`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp).
- App build wiring is under [`Software/App/GlanceStrain-CMake/CMakeLists.txt`](../../Examples/Apps/GlanceStrain/Software/App/GlanceStrain-CMake/CMakeLists.txt).
- Glance assets are compiled from headers like [`Software/Libs/Header/icon_60x60.h`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/icon_60x60.h:1) and the PNGs under [`Resources/`](../../Examples/Apps/GlanceStrain/Resources:1).
2. **Entry point**
- `Service::run()` in [`Service.cpp`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp) is the main loop and the only entry point called by the kernel.
- The loop blocks on `mKernel.comm.getMessage()` and switches on `SDK::MessageType` values (glance start/stop, tick, sensor data, app stop).
3. **UI**
- `EVENT_GLANCE_START` calls `configGui()` to fetch sizing via `SDK::Message::RequestGlanceConfig` and initializes [`mGlanceUI`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp).
- `createGuiControls()` builds the icon, title, and value text controls and stores them in `mGlanceUI` (see [`Service::createGuiControls()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- `onGlanceTick()` updates `mGlanceValue` and dispatches `RequestGlanceUpdate` when the form is invalid; it only runs while the glance is active (see [`Service::onGlanceTick()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
4. **Sensor connections**
- The service owns `SDK::Sensor::Connection` members for heart rate, activity, and touch (see [`mSensorHR`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp), [`mSensorActivity`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp), and [`mSensorTouch`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp)).
- `connect()` is called on glance start; `disconnect()` is only called on `COMMAND_APP_STOP` (see [`Service::connect()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp) and [`Service::disconnect()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
5. **Handle incoming sensor messages**
- `EVENT_SENSOR_LAYER_DATA` forwards sensor batches into `Service::onSdlNewData()` (see [`Service::run()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- `onSdlNewData()` uses `SDK::Sensor::DataBatch` and the parser types `SensorDataParser::Touch`, `::Activity`, and `::HeartRate` to validate and decode frames (see [`Service::onSdlNewData()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- Touch updates `mIsOnHand` and forces an immediate FIT save when the watch is removed (off-hand transition).
- Activity updates `mActiveMin` from the parsed duration.
6. **Apply the strain calculation and accumulate state**
- Heart-rate samples are accepted only in [50, 255]. For each valid sample, `norm = (hr - 60.0f) / 120.0f` and `delta = max(0.0f, norm) * 0.75f` (see [`Service::onSdlNewData()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- Running totals are stored in `mTotalStrain`, `mSumHR`, `mMaxHR`, `mSampleCount`, and `mLastHr` (see [`Service.hpp`](../../Examples/Apps/GlanceStrain/Software/Libs/Header/Service.hpp)).
7. **Emit samples and refresh the UI on ticks**
- `onSdlNewData()` appends pending records to `mPendingRecords` on each activity sensor event, which runs at the `skSamplePeriodSec` cadence (5 seconds), whenever the watch is on-hand, regardless of glance activity (see [`Service::onSdlNewData()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- `onGlanceTick()` only updates the UI and emits `RequestGlanceUpdate` when invalid (see [`Service::onGlanceTick()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- `saveFit(false, false)` is invoked from sensor events to persist at most once per `skSaveIntervalSec` (3600 seconds), and can run in the background when on-hand.
8. **Persist FIT data and handle day rollover**
- `checkDayRollover()` updates `mCurrentDate`, rebuilds `mFitPath` as `strain_YYYY-MM-DD.fit`, and resets accumulators when the date changes (see [`Service::checkDayRollover()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- `saveFit(force, finalizeDay)` opens or creates the FIT file, writes definitions when needed, appends pending records, and optionally emits a session summary (see [`Service::saveFit()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- FIT message/field helpers are initialized in the constructor (`mFitFileID`, `mFitRecord`, `mFitSession`, `mFitStrainField`, `mFitActiveField`) and used by `writeFitDefinitions()`, `appendPendingRecords()`, and `writeFitSessionSummary()` (see [`Service::Service()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp) and helper methods nearby).
- `saveFit()` is triggered by sensor events and can run in the background when the watch is on-hand; it is still time-gated by `skSaveIntervalSec`.
9. **Logs behavior while iterating**
- Use log output from `Service.cpp` (`LOG_INFO`/`LOG_DEBUG`) to verify event sequencing, day rollover, and save cadence (see [`Service::run()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp) and [`Service::checkDayRollover()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- Confirm that off-hand transitions force an immediate save by simulating `TOUCH_DETECT` events and watching for `saveFit(true, false)` calls in logs (see [`Service::onSdlNewData()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
- Verify the FIT output location and daily file naming using `mFitPath` after a glance start/rollover (see [`Service::checkDayRollover()`](../../Examples/Apps/GlanceStrain/Software/Libs/Source/Service.cpp)).
## Common vs App-Specific Components
### Common services and patterns (shared across glance apps)
- Kernel integration via `SDK::Kernel` and `mKernel.comm` message loop.
- Glance UI primitives: `SDK::Glance::Form`, `ControlText`, `RequestGlanceConfig`, `RequestGlanceUpdate`.
- Sensor subscription via `SDK::Sensor::Connection` and `SDK::Interface::ISensorDataListener`.
- File access via `SDK::Interface::IFileSystem` and `SDK::Interface::IFile`.
- FIT helper/definitions via `SDK::Component::FitHelper`. See [Docs/FitFiles-Structure.md](../../Docs/FitFiles-Structure.md) for FIT implementation details.
### GlanceStrain-specific services and logic
- Strain scoring formula and gating by valid HR range [50, 255].
- Daily accumulator reset (no recovery from previous FIT data).
- Developer fields for FIT (`strain` as float32 score and `active_min` as uint32 minutes) via `mFHStrainField` and `mFHActiveField`. See [Docs/FitFiles-Structure.md#developer-fields-implementation](../../Docs/FitFiles-Structure.md#developer-fields-implementation) for developer field setup.
## App ↔ Kernel Interaction / Execution Path
1. **Kernel starts glance** → `EVENT_GLANCE_START` → UI configured and rendered, sensors connected, daily FIT context initialized.
2. **Kernel delivers sensor batches** → `EVENT_SENSOR_LAYER_DATA` → `onSdlNewData()` updates HR/active minutes/on-hand state.
3. **Kernel ticks glance** → `EVENT_GLANCE_TICK` → `onGlanceTick()` updates the UI only. This only happens while the glance is active.
4. **Kernel stops glance** → `EVENT_GLANCE_STOP` → mark glance inactive; sensors stay connected and background acquisition continues while sensor-driven persistence can continue when on-hand.
5. **No glance ticks while inactive** → no UI refresh occurs, but sensor events continue to drive record emission, rollover checks, and saves when on-hand.
6. **Kernel stops app** → `COMMAND_APP_STOP` → finalize session, disconnect, exit.
## Key Interfaces and Data Structures
- `SDK::Kernel` → kernel handle for messaging (`comm`) and filesystem (`fs`).
- `SDK::Interface::ISensorDataListener` → callback interface implemented by `Service` (`onSdlNewData`).
- `SDK::Sensor::Connection` → connects to HR/activity/touch drivers and matches incoming handles.
- `SDK::SensorDataParser::*` → typed decoding of incoming sensor frames.
- `SDK::Glance::Form` and `SDK::Glance::ControlText` → glance UI layout and updates.
- `SDK::Interface::IFile` / `IFileSystem` → FIT persistence.
- `SDK::Component::FitHelper` → FIT message definitions and writing. See [Docs/FitFiles-Structure.md#fithelper-component-deep-dive](../../Docs/FitFiles-Structure.md#fithelper-component-deep-dive).
- `ActivityWriter` → encapsulates FIT file creation and writing. See [Docs/FitFiles-Structure.md#activitywriter-class-overview](../../Docs/FitFiles-Structure.md#activitywriter-class-overview).
## Behavior Details
### Logging cadence
- Record strain samples on a 5-second cadence while the watch is on-hand; cadence is driven by the activity sensor connection interval, not glance ticks.
### Persistence and recovery
- Persist to a daily FIT file (one file per day).
- No recovery of previous state; accumulators reset to zero at the start of each day.
### Gating rules
- Only process HR samples when the watch is on-hand.
- Accept HR values in the range [50, 255] for strain calculation.
- Write FIT records on every activity sensor event when on-hand, with no additional time gating.