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ο
flowchart TD
A[Kernel Starts App] --> B[Service::run]
B --> C{Event Type}
C -->|EVENT_GLANCE_START| D[Configure UI<br/>Connect Sensors<br/>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<br/>Trigger FIT Write if On-Hand]
Q -->|HR| K{HR in 50..255 & On-Hand?}
K -->|Yes| L[Calculate Strain Delta<br/>Accumulate Totals]
L --> J
C -->|EVENT_GLANCE_TICK| M[onGlanceTick<br/>Update UI Display]
C -->|EVENT_GLANCE_STOP| N[Mark Glance Inactive<br/>Keep Sensors Connected]
C -->|COMMAND_APP_STOP| O[Finalize FIT Session<br/>Disconnect Sensors<br/>Exit App]
J --> P[ActivityWriter::addRecord<br/>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ο
Serviceis 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 onmKernel.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 onCOMMAND_APP_STOP.
2) Kernel message loopο
Service::run() handles the following message types in order:
EVENT_GLANCE_STARTConfigure glance UI bounds (
configGui()), build controls (createGuiControls()), markmGlanceActive = true.Connect to sensors (
connect()), handle day rollover (checkDayRollover()), and trigger an initial FIT save (saveFit(true, false)).
EVENT_GLANCE_TICKRefresh display (
onGlanceTick()). These ticks only occur while the glance is active; they do not fire in the background.
EVENT_SENSOR_LAYER_DATADispatch batched sensor samples into
onSdlNewData().
EVENT_GLANCE_STOPMark the glance inactive and keep sensors connected; sensor-driven saves can continue when on-hand.
COMMAND_APP_STOPFinalize 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 aRequestGlanceUpdatewhen the UI is invalid; it only runs while the glance is active.
4) Sensor layerο
SDK::Sensor::Connectioninstances subscribe to:HEART_RATEβSensorDataParserHeartRateACTIVITYβSensorDataParserActivityTOUCH_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.0fdelta = 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.fitin 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::FitHelperdefinitions for file ID, developer data, events, records, sessions, and activity summaries. See 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ο
App layout
Service entry point and logic live in
Software/Libs/Header/Service.hppandSoftware/Libs/Source/Service.cpp.App build wiring is under
Software/App/GlanceStrain-CMake/CMakeLists.txt.Glance assets are compiled from headers like
Software/Libs/Header/icon_60x60.hand the PNGs underResources/.
Entry point
Service::run()inService.cppis the main loop and the only entry point called by the kernel.The loop blocks on
mKernel.comm.getMessage()and switches onSDK::MessageTypevalues (glance start/stop, tick, sensor data, app stop).
UI
EVENT_GLANCE_STARTcallsconfigGui()to fetch sizing viaSDK::Message::RequestGlanceConfigand initializesmGlanceUI.createGuiControls()builds the icon, title, and value text controls and stores them inmGlanceUI(seeService::createGuiControls()).onGlanceTick()updatesmGlanceValueand dispatchesRequestGlanceUpdatewhen the form is invalid; it only runs while the glance is active (seeService::onGlanceTick()).
Sensor connections
The service owns
SDK::Sensor::Connectionmembers for heart rate, activity, and touch (seemSensorHR,mSensorActivity, andmSensorTouch).connect()is called on glance start;disconnect()is only called onCOMMAND_APP_STOP(seeService::connect()andService::disconnect()).
Handle incoming sensor messages
EVENT_SENSOR_LAYER_DATAforwards sensor batches intoService::onSdlNewData()(seeService::run()).onSdlNewData()usesSDK::Sensor::DataBatchand the parser typesSensorDataParser::Touch,::Activity, and::HeartRateto validate and decode frames (seeService::onSdlNewData()).Touch updates
mIsOnHandand forces an immediate FIT save when the watch is removed (off-hand transition).Activity updates
mActiveMinfrom the parsed duration.
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.0fanddelta = max(0.0f, norm) * 0.75f(seeService::onSdlNewData()).Running totals are stored in
mTotalStrain,mSumHR,mMaxHR,mSampleCount, andmLastHr(seeService.hpp).
Emit samples and refresh the UI on ticks
onSdlNewData()appends pending records tomPendingRecordson each activity sensor event, which runs at theskSamplePeriodSeccadence (5 seconds), whenever the watch is on-hand, regardless of glance activity (seeService::onSdlNewData()).onGlanceTick()only updates the UI and emitsRequestGlanceUpdatewhen invalid (seeService::onGlanceTick()).saveFit(false, false)is invoked from sensor events to persist at most once perskSaveIntervalSec(3600 seconds), and can run in the background when on-hand.
Persist FIT data and handle day rollover
checkDayRollover()updatesmCurrentDate, rebuildsmFitPathasstrain_YYYY-MM-DD.fit, and resets accumulators when the date changes (seeService::checkDayRollover()).saveFit(force, finalizeDay)opens or creates the FIT file, writes definitions when needed, appends pending records, and optionally emits a session summary (seeService::saveFit()).FIT message/field helpers are initialized in the constructor (
mFitFileID,mFitRecord,mFitSession,mFitStrainField,mFitActiveField) and used bywriteFitDefinitions(),appendPendingRecords(), andwriteFitSessionSummary()(seeService::Service()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 byskSaveIntervalSec.
Logs behavior while iterating
Use log output from
Service.cpp(LOG_INFO/LOG_DEBUG) to verify event sequencing, day rollover, and save cadence (seeService::run()andService::checkDayRollover()).Confirm that off-hand transitions force an immediate save by simulating
TOUCH_DETECTevents and watching forsaveFit(true, false)calls in logs (seeService::onSdlNewData()).Verify the FIT output location and daily file naming using
mFitPathafter a glance start/rollover (seeService::checkDayRollover()).
Common vs App-Specific Componentsο
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 (
strainas float32 score andactive_minas uint32 minutes) viamFHStrainFieldandmFHActiveField. See Docs/FitFiles-Structure.md#developer-fields-implementation for developer field setup.
App β Kernel Interaction / Execution Pathο
Kernel starts glance β
EVENT_GLANCE_STARTβ UI configured and rendered, sensors connected, daily FIT context initialized.Kernel delivers sensor batches β
EVENT_SENSOR_LAYER_DATAβonSdlNewData()updates HR/active minutes/on-hand state.Kernel ticks glance β
EVENT_GLANCE_TICKβonGlanceTick()updates the UI only. This only happens while the glance is active.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.No glance ticks while inactive β no UI refresh occurs, but sensor events continue to drive record emission, rollover checks, and saves when on-hand.
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 byService(onSdlNewData).SDK::Sensor::Connectionβ connects to HR/activity/touch drivers and matches incoming handles.SDK::SensorDataParser::*β typed decoding of incoming sensor frames.SDK::Glance::FormandSDK::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.ActivityWriterβ encapsulates FIT file creation and writing. See 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.