From 1851fa76e6aa8a2401eccc109c4203603acbab0a Mon Sep 17 00:00:00 2001 From: Graham Jones Date: Fri, 14 Nov 2025 21:25:34 +0000 Subject: [PATCH] Added application structure description file to help new developers --- APP_STRUCTURE.md | 378 +++++++++++++++++++++++++++++++++++++++++++++++ FLOW_DIAGRAM.png | Bin 0 -> 40216 bytes README.md | 2 + 3 files changed, 380 insertions(+) create mode 100644 APP_STRUCTURE.md create mode 100644 FLOW_DIAGRAM.png diff --git a/APP_STRUCTURE.md b/APP_STRUCTURE.md new file mode 100644 index 0000000..b1c7039 --- /dev/null +++ b/APP_STRUCTURE.md @@ -0,0 +1,378 @@ +# OpenSeizureDetector Android App Structure + +This document gives new contributors a fast, high-level map of how the Android application is organised: the major Java classes, resource folders, the startup / shutdown lifecycle, and the data + alarm flow. + +## 1. Top-Level Overview +OpenSeizureDetector is an Android foreground-service based application that: +- Collects motion (acceleration) and physiological (heart rate, optionally SpO₂) data from a wearable (Garmin, Pebble, BLE devices, phone sensors, etc.). +- Analyses incoming samples to detect tonic–clonic seizure patterns. +- Raises local (audible) and remote (SMS / phone call) alarms and optionally shares anonymised data with a central server. +- Provides a swipe-based main UI (MainActivity2 + fragments) and a startup checklist screen (StartupActivity) to ensure prerequisites are satisfied before normal operation. + +Core runtime logic lives in the `SdServer` foreground service; Activities and Fragments mostly visualize status and manipulate preferences. + +## 2. Startup Lifecycle (Happy Path) +1. User taps the launcher icon -> Android launches `StartupActivity` (declared with MAIN/LAUNCHER intent filter in `AndroidManifest.xml`). +2. `StartupActivity`: + - Applies default preference values from XML (alarm, general, datasource, logging, etc.). + - Requests / validates required runtime permissions (notifications, SMS, location, Bluetooth, activity recognition, etc.). + - Starts (or restarts) the foreground service `SdServer` via `OsdUtil.startServer()` if not already running. + - Binds to the service through `SdServiceConnection` to monitor status (watch connection, settings received, data flowing). + - Displays a checklist (ProgressBars + TextViews) updated by a periodic timer until all conditions are OK. + - When all OK, transitions to either `MainActivity2` (new UI) or legacy `MainActivity` depending on the `UseNewUi` preference. +3. `SdServer.onStartCommand()`: + - Calls `updatePrefs()`; selects concrete `SdDataSource*` implementation based on `DataSource` preference. + - Instantiates and starts the chosen data source (e.g., `SdDataSourceGarmin`, `SdDataSourceBLE`, `SdDataSourcePhone`, etc.). + - Initialises logging (`LogManager`), location (`LocationFinder` if SMS alarms enabled), timers, embedded HTTP server (`SdWebServer`), wake lock, and notification channels; enters foreground (persistent notification). + - Begins receiving data; populates `SdData` and runs analysis algorithms (e.g., seizure + heart rate) to update alarm state. +4. `MainActivity2` binds to the already running `SdServer` to present live status via Fragments. + +## 3. Shutdown Lifecycle +There are several ways the service stops: +- User selects an "Exit" / stop option (menu action triggers `OsdUtil.stopServer()`), which calls `stopService` for `SdServer`. +- System kills the service (low memory or user force-stop) -> `SdServer.onDestroy()` releases wake lock, stops data source, timers, web server, and cleans up. +- Device reboot triggers `BootBroadcastReceiver` which, if `AutoStart` preference is true, launches `StartupActivity` to restart. + +## 4. Major Java Components +### Activities +- `StartupActivity`: Launcher activity; initial permission + readiness checklist; starts/binds the service; routes to main UI. +- `MainActivity2`: Modern, swipe-based interface using `ViewPager2` + Fragments; shows system/algorithm status, data sharing, web server info, heart rate, ML algorithm, battery, watch signal etc. +- `MainActivity`: Legacy UI retained for backward compatibility (optionally used if `UseNewUi` is false). +- `PrefActivity`: Preferences editor (headers + fragments defined in `res/xml/*prefs.xml`). +- `BLEScanActivity`: Discovers and selects BLE devices when using BLE/BLE2 data source. +- `AuthenticateActivity`: Handles login for data sharing / remote API. +- `LogManagerControlActivity`, `ExportDataActivity`, `RemoteDbActivity`: Data sharing, viewing, exporting, pruning local DB. +- `ReportSeizureActivity`, `EditEventActivity`: Manual event reporting / editing. + +### Foreground Service +- `SdServer`: Heart of the application. Manages: + - Data source selection and life-cycle. + - Alarm evaluation (seizure, heart rate, fall, etc.). + - Notification updates (different channels for service status, events, data sharing issues). + - SMS & phone call alarm orchestration (timers to allow cancellation). + - Logging & data sharing upload scheduling. + - Embedded HTTP server (`SdWebServer`) for local access. + - Wake lock to keep CPU active during monitoring. + +### Data Sources (inherit from / similar pattern to `SdDataSource`) +Each encapsulates a communication protocol + parsing logic: +- `SdDataSourcePebble` – Legacy Pebble watch integration. +- `SdDataSourceAw` – Android Wear devices. +- `SdDataSourceGarmin` – Garmin watch app (acceleration + heart rate streams). +- `SdDataSourceBLE` / `SdDataSourceBLE2` – Generic BLE device integrations (v1 / v2 protocols for devices like PineTime / BangleJS). +- `SdDataSourceNetwork` – Pulls detector data over network from a remote instance. +- `SdDataSourcePhone` – Uses phone onboard sensors as the detector. + +### Algorithms & Data Structures +- `SdData`: Aggregates current sample values, processing results, and metadata (data source name, versions, etc.). +- `SdAlgHr`: Heart rate alarm algorithms (simple threshold, adaptive, average-based). +- `SdAlgNn`: Neural network / machine learning based seizure detection (see `FragmentMlAlg` for UI). +- `CircBuf`: Circular buffer used for windowed averaging and historical metrics. + +#### Core Analysis Pipeline (`SdDataSource.doAnalysis()`) +The principal seizure detection loop lives in the protected method `SdDataSource.doAnalysis()`, called each time a fresh window of accelerometer samples arrives (vector magnitude or derived from 3D data): +1. Phone/watch battery percentages are updated and appended to rolling buffers. +2. (Currently hard-coded) sample frequency set (e.g. 25 Hz) and frequency resolution derived from window length (`mNsamp`). +3. FFT performed on the raw acceleration window using JTransforms `DoubleFFT_1D.realForward`. +4. Overall spectrum "power" (`specPower`) accumulated up to a cutoff frequency (`FreqCutoff`), zeroing higher bins to reduce noise. +5. Region of Interest (ROI) defined by preferences `AlarmFreqMin`/`AlarmFreqMax`; average ROI power (`roiPower`) and the ratio `roiRatio = 10 * roiPower / specPower` computed. +6. Simplified spectrum (`simpleSpec[]`) built in 1 Hz bins for UI visualisation (scaled by `ACCEL_SCALE_FACTOR` to align with historic Pebble scaling). +7. Populates fields in `mSdData` (timestamps, `specPower`, `roiPower`, thresholds, ROI freq bounds, simplified spectrum array, flags) and marks `haveData`. +8. If the CNN alarm feature is enabled (`mCnnAlarmActive`) then `nnAnalysis()` updates `mPseizure` probability. +9. Secondary narrow-band motion check via `flapCheck()` (arm flapping detection) producing a boolean fed into `alarmCheck()`. +10. `alarmCheck()` applies thresholds (`AlarmThresh`, `AlarmRatioThresh`) and enabled algorithm flags (classic OSD, flap, CNN) to set alarm cause/state. +11. Additional modalities processed: `hrCheck()` (heart rate alarms / frozen HR detection), `o2SatCheck()` (oxygen saturation), `fallCheck()` (fall detection), and `muteCheck()` (user-induced mute logic). +12. Result dispatched upstream via `mSdDataReceiver.onSdDataReceived(mSdData)` (SdServer consumes to raise notifications / alarms). + +Key preferences influencing `doAnalysis()`: +- `AlarmFreqMin` / `AlarmFreqMax`: ROI frequency band. +- `AlarmThresh` / `AlarmRatioThresh`: Power and ratio thresholds for alarm state. +- Flap detection thresholds (`FlapThresh`, `FlapRatioThresh`, min/max flap band) when flap alarm active. +- Flags enabling algorithms: `mOsdAlarmActive`, `mFlapAlarmActive`, `mCnnAlarmActive`. + +Performance / Extension Notes: +- Current implementation re-allocates FFT arrays each call; optimisation could reuse buffers. +- Sample frequency is hard-coded (25 Hz) inside analysis; aligning it with dynamic settings from watch would improve fidelity. +- Multiple ROIs could be generalised (current flapCheck duplicates spectral processing). +- Scaling (`ACCEL_SCALE_FACTOR`) is applied post-hoc; future refactor could normalise early and adopt floating-point consistently for UI. + +#### `doAnalysis()` Invocation & Alarm Propagation +`doAnalysis()` is not called in a tight loop by the service; instead each concrete `SdDataSource` decides when a complete window of samples is ready and then invokes it: +- BLE (`SdDataSourceBLE` / `SdDataSourceBLE2`): Acceleration notifications fill a raw buffer (`rawData` length 125 = 5s @25Hz). When full, the datasource copies buffered samples into `mSdData`, sets `mNsamp`, calls `doAnalysis()`, then sends a one–byte alarm state back to the device via a status GATT characteristic. +- Phone (`SdDataSourcePhone`): Collects accelerometer sensor events, performs crude downsampling from ~50Hz to 25Hz. Once `rawData` is full it triggers `doAnalysis()`, resets counters and continues. +- Pebble (`SdDataSourcePebble`): The watch app performs analysis on-device and sends already processed results (including `alarmState`, spectrum) – so `doAnalysis()` is NOT used for Pebble; received data directly calls `mSdDataReceiver.onSdDataReceived`. +- Network (`SdDataSourceNetwork`): Fetches remote JSON; if successful passes parsed `SdData` upward. Remote faults set `alarmState` to NET FAULT (7). Local `doAnalysis()` not used. +- Garmin (`SdDataSourceGarmin`): Similar pattern (buffer fill -> `doAnalysis()`). + +After `doAnalysis()` completes in a source that uses it: +1. Spectral metrics (`roiPower`, `specPower`, simplified spectrum) and timing fields are populated. +2. `flapCheck()` optionally computes a narrow-band flap detection boolean. +3. `alarmCheck(flapDetected)` applies power & ratio thresholds and accumulates time in alarm (`mAlarmCount += mSamplePeriod`) to transition through: + - OK (0) -> WARNING (1) after `mWarnTime` seconds of continuous in-alarm condition. + - WARNING (1) -> ALARM (2) after `mAlarmTime` seconds. + - Recovery logic: leaving in-alarm state downgrades from ALARM (2) to WARNING (1) (simulating a just-entered warning), or from WARNING (1) to OK (0). +4. Other modality checks may elevate alarmState: + - `hrCheck()`: If any heart rate alarm stands (simple / adaptive / average) sets `alarmState = 2` and appends cause tags (`HR`, `HR_ADAPT`, `HR_AVG`). Null HR may either cause alarm or fault depending on `mHRNullAsAlarm`. + - `o2SatCheck()`: Low or null oxygen saturation (with null-as-alarm enabled) sets standing flags and may escalate to ALARM. + - `fallCheck()`: Sets `fallAlarmStanding` and may signal FALL alarm state (3). + - `muteCheck()`: Watch/user mute sets `alarmState = 6` (MUTE) overriding other transient states. + - Fault timers (`faultCheck()` elsewhere) may set FAULT (4) or NET FAULT (7). +5. The datasource calls `mSdDataReceiver.onSdDataReceived(mSdData)` (implemented by `SdServer`). + +##### Alarm State Codes (as observed in code) +| Code | Meaning | Origin / Trigger | +|------|---------|------------------| +| 0 | OK | No current alarm condition or post-recovery. | +| 1 | WARNING | Thresholds exceeded for > `warnTime` but < `alarmTime`. | +| 2 | ALARM | Thresholds exceeded for > `alarmTime`, or HR/O₂/fall promoted, or HR adaptive/average thresholds stand. | +| 3 | FALL | Fall detection logic sets `fallAlarmStanding` or explicit fall state. | +| 4 | FAULT | Internal fault (e.g., missing data, HR sensor failure without null-as-alarm). | +| 5 | MANUAL ALARM | Raised manually (e.g., `SdServer.raiseManualAlarm()`). | +| 6 | MUTE | User/watch initiated mute; prevents audible alarm but maintains monitoring. | +| 7 | NET FAULT | Network datasource error / fault condition (`SdDataSourceNetwork`). | + +##### How `SdServer` Reacts (`onSdDataReceived`) +`SdServer.onSdDataReceived(sdData)` interprets `alarmState` plus standing flags and performs side-effects: +- OK (0): Clears `alarmStanding` unless latched (`mLatchAlarms`) from previous alarm or fall. +- MUTE (6): Sets phrase "MUTE", suppresses alarms and notifications severity. +- WARNING (1): Plays warning tone (`warningBeep()`), logs (if enabled), updates notification to warning channel/state. +- ALARM (2) or MANUAL ALARM (5): Sets phrase "ALARM", raises `alarmStanding`, plays alarm tone (`alarmBeep()`), shows main UI, posts high-severity notification, initiates latch timer (`startLatchTimer()`), and sends SMS / phone alarms if enabled (rate-limited to one per minute). +- FALL (3 or `fallAlarmStanding` true): Behaves similarly to ALARM but with phrase "FALL" (alarms + SMS sending). Fall may remain standing until cleared. +- HR / O₂ / Adaptive HR / Average HR: These set `alarmState = 2` when standing; `alarmCause` accumulates tokens; downstream handling identical to ALARM. +- FAULT (4, 7, HR fault, frozen HR fault): Plays fault warning beep (`faultWarningBeep()`), shows fault notification; may attempt datasource restart after timer (auto-restart currently disabled for BLE2 to prevent duplicate notifications). + +##### Latching & Reset +With `mLatchAlarms` enabled, returning to OK does not immediately clear previous ALARM/FALL states; user must manually accept/reset (e.g., via UI actions) or wait for latch timer expiry (`mLatchAlarmTimer`). Without latching, state machine freely transitions downwards. + +##### Data Sharing & Logging Post-Analysis +Upon each received dataset, `SdServer` updates internal `mSdData`, pushes it to `SdWebServer` for external viewing, and passes it to `LogManager` (`mLm.updateSdData(mSdData)`), which may create/append local events (especially on transitions into ALARM states) and schedule remote uploads. + +##### Device Feedback +BLE/BLE2 write a single-byte alarm state back to the watch/device after analysis (`executeWriteCharacteristic(mStatusChar, statusVal)` or peripheral write) enabling haptic / on-watch UI feedback. +Pebble handles its own alarm transitions internally before sending results. + +This separation lets wearable implementations stay lightweight (simple streaming) while centralizing threshold timing, multi-modal fusion, and alarm escalation logic on the phone (except for Pebble legacy analysis). + +### UI Fragments (used in `MainActivity2` ViewPager) +- `FragmentCommon`: Overall status & key indicators. +- `FragmentOsdAlg`: Seizure algorithm metrics (spectrum ratio, thresholds, raw/processed values). +- `FragmentHrAlg`: Heart rate algorithm status & thresholds. +- `FragmentMlAlg`: ML model results / confidence scores. +- `FragmentBatt`: Watch + phone battery status. +- `FragmentSystem`: System info (permissions, service state, logging flags). +- `FragmentWatchSig`: Signal quality / connectivity indicators. +- `FragmentWebServer`: Local web server URL / status. +- `FragmentDataSharing`: Data sharing setup state, counts of local vs remote events. + +### Utilities & Helpers +- `OsdUtil`: Starts/stops/binds the service; permission checks; logging helpers; system/environment utilities. +- `LogManager`: Handles local + remote logging, event packaging, pruning, and upload scheduling. +- `MlModelManager`: Manages loading / inference of ML models (if in use). +- `LocationFinder`: Acquires GPS coordinates for SMS alarms. +- `WebApiConnection` / `WebApiConnection_firebase` / `WebApiConnection_osdapi`: Remote data sharing / API integrations. +- `SdServiceConnection`: Wraps service binding / connection callbacks, exposes convenience methods (`watchConnected()`, `hasSdData()`, `hasSdSettings()`). +- `BootBroadcastReceiver`: Auto-start on device boot when preference enabled. +- `GattAttributes`: BLE UUID constants and attribute names. +- `OsdUncaughtExceptionHandler`: Crash reporting path (uses UCE Handler library). +- `SdWebServer`: Lightweight embedded HTTP server (for local status / data access). + +### Data Sharing Module +Under `uk/org/openseizuredetector/data/...`: +- Repository pattern for authentication: `LoginRepository`, `LoginDataSource`, `LoggedInUser`, `Result` (standard wrapper around success/error). + +## 5. Resource Folder Structure +``` +res/ + layout/ Activity & Fragment UI XML (e.g., startup_activity, activity_main2, fragment_*). + menu/ Action bar & overflow menus (e.g., main_activity_actions.xml). + values/ Strings (`strings.xml`), styles, colors, dimensions; base resources. + values-XX/ Localized strings (de, es, pl, ru, sl, sv, etc.). + drawable/ Icons and graphics (e.g., star_of_life_48x48). Might also include vector assets. + xml/ Preference definition files and network security config: + - alarm_prefs.xml + - basic_prefs.xml + - general_prefs.xml + - logging_prefs.xml + - pebble_datasource_prefs.xml + - network_datasource_prefs.xml + - network_passive_datasource_prefs.xml + - seizure_detector_prefs.xml + - preference_headers.xml (groups preferences) + - network_security_config.xml +``` +Other notable folders: +- `assets/` (if present) – Additional static assets (not heavily used here). +- `libs/` – Third-party JARs (e.g., FFT / chart libraries) bundled with the app. + +## 6. Preferences Flow +1. XML files under `res/xml` define keys and defaults. +2. `StartupActivity.onCreate()` calls `PreferenceManager.setDefaultValues(...)` for each preference file (once per install/version). +3. Classes such as `OsdUtil`, `SdServer`, `SdAlgHr` invoke `updatePrefs()` to read `SharedPreferences` ( + `PreferenceManager.getDefaultSharedPreferences(context)`), caching operational parameters (thresholds, window lengths, flags). +4. Preference changes may trigger service restarts or algorithm behavior changes (e.g., enabling SMS alarms requires location permission and `LocationFinder`). + +## 7. Data & Alarm Flow +``` +Wearable / Phone Sensors --> Concrete SdDataSource --> SdServer (receives callbacks) --> + Algorithms (SdAlgNn, SdAlgHr, fall detection, etc.) --> Alarm State Transitions --> + Audible ToneGenerator / MP3 playback + Foreground Notification Updates + Timed SMS / Phone Call Alerts (with cancellation window) + Data Logging (local DB) / Remote Sharing (LogManager, WebApiConnection*) + Web Server exposure (SdWebServer) +``` +Heart rate buffering uses `CircBuf` windows for simple/adaptive thresholding; seizure analysis (frequency spectrum, ratio thresholds) executed inside data source analysis routines (see respective `SdDataSource*` classes). + +## 8. Foreground Service & Resilience +- Service runs in foreground with a persistent notification (required for stable long-running monitoring on modern Android). +- Wake lock prevents CPU sleep during monitoring sessions (battery intensive but improves reliability). +- Timers manage periodic tasks (event validation checks, remote upload scheduling, alarm muting windows). +- Boot auto-start via broadcast receiver ensures continuity if user opted in. + +## 9. Adding a New Data Source (High-Level Guide) +1. Create a new `SdDataSource` class implementing the expected interface / callback pattern (see existing sources for template). +2. Handle connection, authentication/handshake, data parsing, and call back into `SdServer` with new samples. +3. Add a selection case in `SdServer.onStartCommand()` for your `DataSource` preference string. +4. Provide any additional preferences XML (e.g., update period, device address) and add them to default initialization in `StartupActivity`. +5. Update UI fragments if device supplies new metrics. + +## 10. Where to Look for Key Logic +- Service lifecycle & alarm orchestration: `SdServer.java` (`onStartCommand`, `onDestroy`, timers, notifications). +- Startup readiness checklist: `StartupActivity.serverStatusRunnable`. +- Data source selection: `SdServer.onStartCommand()` switch over `mSdDataSourceName`. +- Heart rate algorithms: `SdAlgHr.java`. +- ML / seizure algorithm UI: `FragmentMlAlg.java` + `SdAlgNn.java`. +- Permission checks: `StartupActivity` & `OsdUtil` (Bluetooth, activity recognition, SMS, location). +- Logging & data sharing: `LogManager.java`, `RemoteDbActivity.java`, `FragmentDataSharing.java`. +- Embedded web server logic: `SdWebServer.java`. + +## 11. Key Preference Examples (Selected) +| Preference Key | Purpose | +| -------------- | ------- | +| `DataSource` | Selects which device source (Pebble, Garmin, BLE, Phone, Network). | +| `AlarmThresh` / `AlarmRatioThresh` | Seizure detection thresholds (spectrum amplitude / ratio). | +| `HRThreshMin` / `HRThreshMax` | Simple heart rate alarm bounds. | +| `HRAdaptiveAlarmWindowSecs` | Window size for adaptive HR average buffering. | +| `SMSAlarm` / `PhoneCallAlarm` | Enable remote alerts. | +| `LogData` / `LogDataRemote` | Enable local logging vs remote data sharing. | +| `UseNewUi` | Switch between legacy and modern main UI. | +| `AutoStart` | Auto-launch on device boot. | + +(See `res/xml/*_prefs.xml` for full list.) + +## 12. Notifications & Timers +- Multiple channels for service status and events (IDs inside `SdServer`). +- Timers: `FaultTimer`, `CheckEventsTimer`, SMS countdown (`SmsTimer`), latch alarm timer, etc., each controlling asynchronous transitions. + +## 13. Data Sharing Flow +1. User authenticates (`AuthenticateActivity`) -> obtains token stored in preferences. +2. `LogManager` packages events (timestamped, with retention pruning) and attempts periodic uploads (`remoteLogPeriod`). +3. Unvalidated remote events prompt UI reminders (`FragmentDataSharing`). + +## 14. Crash Handling +`UCEHandler` integrated in Activities and Service to capture uncaught exceptions and offer sending logs via email. + +## 15. Embedded Web Server +`SdWebServer` exposes (read-only) status / logged data for local network access; started automatically by `SdServer` after data source initialisation. + +## 16. Stopping / Restarting Service Programmatically +- Stop: `OsdUtil.stopServer()` -> calls `stopService(Intent(SdServer))`. +- Start: `OsdUtil.startServer()` -> `Context.startForegroundService(...)` (on modern Android) then service builds notification & begins monitoring. +- Restart triggered implicitly if critical permissions change (logic can call stop/start to reinitialise components). + +## 17. Extending Alarms / Algorithms +To add new alarm logic (e.g., oxygen saturation): +1. Introduce algorithm class (`SdAlgO2` style) storing buffers and thresholds. +2. Update data source parsing to capture new metric. +3. Integrate into `SdServer` evaluation loop; amend notification text generation. +4. Provide preference keys + XML + UI Fragment display. + +## 18. Glossary +- "Latch Alarm": Alarm remains active until explicitly reset (even if underlying condition clears) for a configured duration. +- "Adaptive HR Alarm": Builds a moving average; raises alarm when HR deviates beyond +/- configurable delta. +- "Foreground Service": Long-lived component with persistent notification; less likely to be killed. +- "Data Sharing": User-consented upload of anonymised seizure events / sensor data to central server for algorithm improvement. + +## 19. Useful Entry Points for Debugging +- Set breakpoints in `SdServer.onStartCommand()` to inspect data source initialisation. +- Use logs emitted via `OsdUtil.writeToSysLogFile` to trace state transitions. +- Inspect `StartupActivity.serverStatusRunnable` for readiness gating issues. + +## 20. Related Documents +- `README.md`: General project overview, build instructions. +- `DEV_NOTES.txt`: Developer notes / historical comments. +- `BLE_Datasource_Specification.md`: Protocol specifics for BLE devices. +- PDFs under `doc/` for algorithm assessment and app structure diagrams. + +## 21. End-to-End Data -> Alarm Flow Diagram +Below are two complementary diagrams (ASCII and Mermaid) showing how a data window travels from the wearable to an alarm being raised. + +PNG Version: See `FLOW_DIAGRAM.png` in the repository root for a downloadable image. + +ASCII Flow +``` +Wearable Sensors (Accel / HR / O₂) + | + v +Watch Firmware / Device App + | (Pebble: does analysis + sends results) + | (BLE/Garmin/PineTime/etc.: streams raw samples) + v +SdDataSource (Buffer + Parse + Downsample/Scale) + | (Collect ~125 samples = 5s @25Hz) + v (window full) +doAnalysis() + |-> FFT (JTransforms) & Spectrum Power (specPower) + |-> ROI Power & Ratio (roiPower / roiRatio) + |-> Simplified Spectrum (simpleSpec[]) + |-> flapCheck() narrow band detection + |-> nnAnalysis() (if CNN enabled) + |-> hrCheck(), o2SatCheck(), fallCheck(), muteCheck() + v +Populate mSdData (specPower, roiPower, simpleSpec, alarmState, alarmCause, metrics) + v +mSdDataReceiver.onSdDataReceived(mSdData) + v +SdServer.onSdDataReceived() + |-> Alarm state machine (OK/WARNING/ALARM/FALL/FAULT/MUTE) + |-> Latching logic (startLatchTimer if ALARM) + |-> Notifications (foreground + event channels) + |-> Tones (warningBeep / alarmBeep / faultWarningBeep) + |-> SMS / Phone Call (rate-limited, if enabled) + |-> Logging & Data Sharing (LogManager update, remote upload scheduling) + |-> Web Server update (SdWebServer.setSdData) + |-> Write alarmState byte back to device (BLE/Garmin) + v +UI Fragments / Web Server / Remote API Consumers +``` + +Mermaid Diagram (optional rendering if supported): +```mermaid +flowchart TD + W[Wearable Sensors\n(Accel / HR / O₂)] --> WF[Watch Firmware / Device App] + WF --> DS{SdDataSource\nBuffer & Parse} + DS -->|Window Full| AN[doAnalysis()] + AN --> FFT[FFT + Spectrum] + FFT --> ROI[ROI Power & Ratio] + ROI --> ALG[flapCheck / nnAnalysis / hrCheck / o2SatCheck / fallCheck / muteCheck] + ALG --> SD[SdData Populated\n(alarmState, metrics)] + SD --> RCV[mSdDataReceiver.onSdDataReceived] + RCV --> SRV[SdServer.onSdDataReceived] + SRV --> ACT[Alarm Actions\nNotification / Tone / SMS / Phone / Latch / Log] + SRV --> FEED[Write alarmState\nback to Device] + ACT --> UI[UI Fragments / Web Server / Data Sharing] + WF -. Pebble path (analysis on watch) .-> SD +``` +Notes: +- Pebble path bypasses local `doAnalysis()`; analysis executes on the watch and sets `alarmState` before dispatch. +- Network datasource substitutes "Buffer & Parse" with remote JSON fetch and may directly set NET FAULT (7) on failure. +- Latching prevents immediate clearing of ALARM/FALL states until user intervention or timer expiry. + +## 22. Related Documents +- `README.md`: General project overview, build instructions. +- `DEV_NOTES.txt`: Developer notes / historical comments. +- `BLE_Datasource_Specification.md`: Protocol specifics for BLE devices. +- PDFs under `doc/` for algorithm assessment and app structure diagrams. + +--- +Questions / Improvements: Feel free to open issues or pull requests on GitHub. diff --git a/FLOW_DIAGRAM.png b/FLOW_DIAGRAM.png new file mode 100644 index 0000000000000000000000000000000000000000..bc20efccc874622c9fb97cc20ac3a82b65469ca2 GIT binary patch literal 40216 zcmd43cR1H=|3Ch+g-}V95Rzoeh|Fkcm?2~(q(~%tW-CHSl2OV?lFW>Zl5EjHBwNWA zGQJO8_rCAXpTFbyUB`Xgm+R`n>wUh?^Z9(N=NY1L>c~c#?KA{IY&>>UMT;OvNeF`E zJ~b)+jh~UK96{XcJ*J|h<8p7j%+*M{c6Ci*kHD@Cn>qP5X>QuquEiwkQ{=;`uNbN- zn696pKU$`HWp8Nx%7SfvYId2B>Vm#tw?SxXTj%yyT>M`HZ*g)KJTRpeyczTsT*4j+6GOhWdWASd;0 zG18#JKTIC>`ZdMxgdVxe z1_pQY^70}hBje-GC@L!2+1X8eQ23;uXL+-tqGEaK;X@v}>ccl!cEzauF4-toa(D0E zy)1ztugwXckdR}FibQ0`=g%Q#ELUm`^z~oK#Ge=%sEc;~@o8#l?xIWwDe?UI^Pz_L zYRkMJ299#OFC~v2@um*sSv5;u`Xq9pjfXzyoUN_rk59Qzp9(pgc5-qu68g0?_g&7O zgfvCIXuGtubm^}icQGoHa*qu}@QoWfk1b<9zjPK3s!kDdT$nVIkQJ&9pxq`yCu{#z z(bjf6Y@6_h9FykZ8TZ`P%0PXA4{dF33zI1xA08)py&H5OAqp?|{!Dt)QhL{MxXH=c zIVj2X;QfjUg~*Q9RuY1GV?(Y54t3!=DQZ(KUIt(`@9)H}~)~4m@xpM2) ztw)a@Q8BQexF;RC+?#9Pr)+50)zHu|G&FQPPUwpCy<4{!<2zS}y;p1Bym^$B6?Uq2 zQ?Rj8c23o+SFbX(AHQSsSMXh1=x1=U(TyhwAqfu;r)3u48|P*SeWeKfH+L^GkOmvy_dv(vAfaeu!+*kSb#WNK_L+o~RGeCx_v+%F z;wsGthZ_@RfBl}Q&`U|A_P0sjN`_UN#lFSf=<0g$oBf7VHfS49=WkO=$F*DEl>#6%&-2p_ZGMcl^|;rsih7luqoY z2S*}k0;=|?-?x{(7qd@qGxzA+++0sj&-+Iw_X!IN^YPg`J1aPiF*R%sdyVzSy2ixB zu(GoL`uSyMdOC{3Z{mYHQ%74L_S(*!J11#RQKo35r=>|?P4x6W`uUMg|FYEA|MVf- z5D%TC;EkWrG(2;;w>*2lUSgQChK9zi+qYkws|{yAv+1Dc5)&bE?T7xD&C8OKclGss zwhVnUi!w4YVil*T#l*!?Fp949TZA{qix1-$CMG6k<}>?_h>jgp30C1E6qS@Jv7h6F zO`0skj;pB^*mg;tf5)V6`uX|!2ZwKR-4EZfW5*c*-Ak9Ac$_Y@`$D=V7woaN>Pft+ z3ZRXVyngAW^Mo<+sv}F!O!vZty#yyOZ=g|mS1Z}l$(Z5Kc~;nTEP)3O90&*qIDh_p z>k~;XE>hxFTpU&Oh7B7CpU3J+RXq3D8|vyJ70#KOawauTjEwly-M-EAXLo4&RIRVn zE&+iP>gqXN0!NP2#*15hlQ!1TAt!=EL;Y%#+_GWKY6-o?Cy(+-R$!R*) zo}ra-Te_-$YG7^CK&>A(aohX%M{cn6_4hw`(fQ`hVd9lllILNe>OqH>`Hc+?H6oXA zW~R@e7F}P{W|C9pblc=q>qiiHb>_J)J*BSiAFFQ*dyVa1qu7GCe(>PI;K%2L;$%@| zrOh~F)FO7uO4@e5FslwUW07>9zeM;fliqk8wyi!%FC+WG1MX!JuVt4lTcS%#52ksh zSZ8Xzxxvc4IpnEH#m^rgooz@)ABjeq1_o&t=K!9ri+wNj$PkKH<6pd4!CBt-M@$&(+;i!*(FePS1b28PMrzUnx6g7^LF*XI|f z2SY+a_GiVIr0{q@EGW3vk$HLx?+Kp@!a_t;l!VaH(|iAui!_DV<~2b^My9Q;%|iBL ze0(Pxn+lia`SZJPY&?7R?9H1ub8~Z9LY9|3q?eXE^h^SRf+o6)^PWH7yynh(#%uq% z_H@nZ;k1H|j*jzrpHb_EjKQ^}-b>+;3`YYu~=L{_x}s&e-)cCr_Rn5H_a|dhp-@nRjT^=$~~V zshzU>XwJKP_pvlBbNlX&jyW6$K|w*nr=&!N@M*Zc!8g@tPMC)H`aP#YQc4OP@qp(R zg7Bcfb(H7#{CL=n8x3#YY8YgfmzT%J>N+}#7waECPHVJVwWX)$=kV~y?rs4lnxKt4 zc)e{F-lu&0D8$Ves_|=QZpfBBJ>{Oi=e~cGl$1mfAo~jxK0AO6^=G0defI=>8SU|@<>`;tXtRfns(uSs6ru4{gLR05Bm3$I|Ufx@_ zpo%3qIk~<#Cl3#gix$wMxldrnDy%M?)7NL&bl#mjp|L0b;z#U| zL(0mXU0oSj9c^tW_5QS6oOorZUNz#PJkrwoJe$v!Ts<}W{P~UV-uZF7d-u-f;zjJ+ zy5hr!4=dVrhoJZ#aGB&>lqGE{-_F94enyZasCsH<=J$`hW`2JDuiAc!oNwQ~dR-GvKgx%wg^*hnHQbgFT8xZ=Wp zm6YARb-7|Cwtn#GnY=*Dk0pa)!sUFz!Vw`Mng-*36toTw4#8VbPNurJxG+-8|M=_( zu;uUXe*-ej)fEy%vWBg3OR6k>=FNYJQb<$*EctJTt=0e9f^#K ziz_epa&;|kv*G6Ax^se8lVSNok>fD^iPhEL6G>i68Ao}dj`A?DAG{-Jd-w6XVKKaU_0^k$LG9(>u5{p z5PnHgmwOs4^SlAAO3;Dcy-QE$Gw-?271yLXI&XtoZDImE+&ix07}3M<>WW9xy#r`n zwwEtIep*me#8ST4(cV5g()!?7G;ieaKwolf!!)_;P*Y;p_n^8+sXT<@D8Zc(X~_v9URME>7=u(vHzGHZH%?|E4Z_&-c;Mo9uFK z=zPB>diBOgjLy(mU%VI=9$t`_cP{TU-rcTUy9V?^g-pr?7^&}^c$AXT7rJ#{W>!|+ z$<#@9r~IpvzkbEJ9=S9#)Cf%Y^l7-qVca8SM z5b0?hon+@4#ud82EU6Q|Es73~j^pFw>u=9GNPuT^9e(r2r)S;2zVfX!5v;6Ooy+O< z`Ynj9U8!gu9_&T>n(*g;awhLlaWSiq<&jw$aD)CgH-C>8_UgDNvHEJ<^o<}v!KbF7 z@yisT1`QigZ{glU>Z@oi#77A>=0D_TPW*rTkLWGz5kMU>i~W}*-GQ(6@7pUVI6gW= zLPgKk^8Wqz$PEvlJ!6bMH}&oHfS8bwkl2wPJ1s3O5xOm5uhovrKuw{F_B>F3X%*REZ=O7r{o@5222FJej-8s3-PmzwLbBsm(p0ucxl->~M4VwMuUUBls$$2Ix zw0L)yIDN+f#`}73IW;ZqyqVe5V4TT^_V(qOq&1JVm0zgm?rEYcKNbfD22inUU%%cE zP=z8iH=KdTKQwWW>OUz4VSV|sg6s6dvNE}#>b@zL9B^U)h0ef0g~Jbi)V$LkYioq^e+90Qb7^7ADC?DO**o0@bVb2F+wIK(I&&cj3l z_E|YRv*@w9y7HTZNJ~p&38Ww=XJKV+ye}s!Bhv}C*48$S&kdjqdiB`jbj=l-HH{=D zV)APxnTd&sP+WbVJ@`(2ef{YX2Nf0n>({T_?D#}UPQG74VncxPx1)t8Gjno2c66-# zs#rT&kSOoDM8)Bu$de(beq~4c5C7%m<2^#2wgqP_U)Tvf&v0;zm!Pq-#-g>GdeozA-@JH zp<)MY+WO!~fO22-9(8H9rgt&>IygE4Y{8Ts@#S6al_z|T@@x)K!6|Mr+V0%6sc&H5c62ni_8}a_)2CyiqqpRHr>h$n8T}X-sId^ErKPQZ_l}N^ z4$u>5(5=1K&(H7S!-qR%ug$&H=5oSYriro5gD$Y%FF zBsiEL1_lQ!EtHi>iLcmRYCG0>gDV4e@U42hx8F-k{Ljx8*{3v2MMZ5+-MnvKR!fTp zTkWs$E~|6r90%TR0{h9(Q4?AN4n60Ia_e)bGqgOG&{0rtfm0-K1oU4 z463)}56yMYR;MfIKTD`A#q=Tw`IX+#w=IEFg9j%QQ2u7}_w=JUf z`o%PW5P)EpgY3!U9E;J$#SX8ltFP;`y&iPfbMh{ag47|UGiN?yH&$0yi_$%~tQuIk zkdqLUdJITafcZPno5;RhyZTms^;+e9R#R8n5*f&!3C!-(TuH!M}c}tav=iF%*mxZQ8 z*?s%=U11AaOS(#FJ2o~(5TNOyG$SJ;))&`${Uw)~zmJV+Jr>^2Dq+KpxP$Ke+ zi@{CmM-$!>doF|6-CB`qWWv8VD z@V&Ck!{G{f7#XQ~^5n)wd_L=xpR(rAX-YkR&jXvGOth2Y5 zgML=;^y$_o%&RA!K7Fd2=UN6#OTSWuB_#Fb+_PuT?%jj$)fLW*u;93HMN*%lBsy0k z4`jr~GRM__DX>jw5I<{f&ea%Lh9(5~K}So={Vkp5pvM9uA!6IPU*;qYHTB!NI@hJy z3(CrVC}iN)qxby2Nov$?nAEEPM7zO;wvwHlt(RlGn+Cemi^9S!b%B9_o460RG+=u& zmobw0EBD=C6@QVR4_J;}n&iED>;}a#p3U?@hnUs{o%0RoB4};gCfQ+OwAU+d2`dcP zJ2(LRojP_5Ki(C62K)>CFN7tzT_M!o(eb6%icENaX8u^$<%&Uv;)@@iXg0D%-MVFJ zYWmE&l}qO2^z^ith={cFPl!Lmu>xn5S9hKynX^s4y9E_WOI!Q8pI`8^*0#1P^IVI- z>j1!CgT8(H2JD7{@V*%j&N%kQ2gtAE<40L(X@U?C6pTM%X=Sx%itX#@=;BPX!tl@# zXcNE_+o_-^kV!PLriO;wtaZ81ofdb^Aon5D5c)QOpJ*9l3j-#@r%98*){3O8jbIA0&z*474Y28PmFRb@WfmbTwTBO~UoJ^^K1= zTIPkM@q)H0DJdOfS2r=~t*@u`s!e1u^sUT%#PaIKPz=CxZEpr?zr7S@~5;A zY#T>{jnAXU=QVd~?sDPQ=5b##D#{YU>$lO3jvPMxuJq|q@ol~=4(VxI@&AzHa3p>0 zC`f%-1USd7fmvIODCH?n+z}zM*_Hn0uEfRVm6eczltV$m9p4VJH`mpL429O=;A>hZ zX%3eZ6*2A^*X*a-;rS(US4w>uns#Uk`AVifg>xfw1-G7hr#T~boN zW=G$fcuWN}OhF9%x~@uDoI96yb+iq7g}g20y`6Gcj->l(94;4S#C&E0*knBFJb+@Y zv&d9(#9y3y1rj&hm~ccO|F&=QFh0=q;&OU=x|bbGsv+bm36Da`m@|cTXCVQ5bg=o3 zs0P~8rDtca;4tew(O1$u43gH?7PXwZH@|c8KNaD3?S~Vyf{i!1#`+pu-A*sS*Y-h| zU;m5X=8&3KuM`i{bU`zD^Cs|UPl;1K)YFz0d#Fv=e-@u9ZJexW4%uMWq7>>2Fh|tV z($Vo;Jgh0Jv}x?E=dTR5CG=lVwBKkc?`P>qm7;gCkv%>8@a`YV1Y1qB6=_v2HETn&9{19lxL85{R9{x6-C zDaNxH`7wp@hq?dS^%= zYk=huKH^qQuZ(E&`^wyZkrGdh%V_e{jvpr>01`!xCDQsU+7i53$>x{4C&>4nO4U|Ki2|JIg;`PF!_z+OX-8^trv)Zw zRWdCt94H^uG@;+A_JUq{muk%|OX^yJt#+%RnSV10iqV-fXH*D{Ovxu%S;v|4rNqRz z*0yL3Upt-?e9$>etMvknt!)fN8Ob^bgP6EEevv zg`4P&K>F`UFhogs+#p@ya~31Ecc-1QFD=4t(H51)_h-`Qm+tNiTRcqcx!yzBHpe}n=rM+J(M!s--5u0BrZG&;b- z!UE`&H6(o7hIX$iR}GjE)Mv~{DW zbngjaiHLbc3A#32iEK;v-Nke*NrpZpI9qcf&K=1oS!ILv}6zh?JF;l}xa;vt!@9Sy|v= zif+&&5uJB~^>GxWB++&I_wP5#HCJ_Vl88)u{;KG%L$4=Kkg=v_rKyO}*%Foa;?(3P zDUD^_AfcnU!L87(gt+^$r)QnLj*t}hrayV|;@LB4ID0F7Uj9qV-%Zw*Ac6qdl5RaHB2$c4LTIO@+ z=9DEH?p8ssDgXWRrE`z#9#PRvH`RXaImkW%#zppprlxDd(iz|6`bh30_!Ksf+ z8{^CQ(u8NHdU+`myjyz6sBHqT(ItOUAgZ6iz+1sO+FZJ{jP2$&^$;3P2}HK=owC8) zy&f~bIh7Eat(xv-Jbny>7kl@v8guN3Y`e}t+yw2nak6napJY?i8kyfzikc=eT+Oo} zl=k-acNDzkuki5lzWMywwY%s_O(f0;tX#4d#_vJG!O%oNh9?){byRq-c@c`(&VgoN zzv~6Ysu`$VQd~Um#S5MnSNWJ0M~@HJ{|YcKB%7O?a^I=iLl!ERb2W%ak59osI%8;fDAG(Z<1NF;vGqrADimV8C!pk1~_g29i#9BIQ=O*M^gO%U*i>6c$PqJ6l>> z!dFxg(z#gdFc|7#J4tbl)vmi}m$txP9+-vaBJ4!MhwH2h>%ibp-|;HiZ9=#Ap3%}G zUDfR1{=@DihkWSN zoMklzIL1D-O_iQFV&5sXA$*=)kUFF-63!+nDH*8V zJ2nH5lmNNs=xZfGll1Sj>*0o)D;`Kfh#b?l^MdchG`zb6!HUM+kJVm}4E|0XvZEqP zzto-mi^C)WcL-k=_#f<2L8dgBAZbe)YR1{=6^kxQC{;5N?l)@t`&`F%lBR2xbop|vX&mZX zG%gNUKNhNVeJrM+3d6(OI5>nI(Nc%)RbUcLgQ6>}%tkjb7&|*VPruri)V$|;vbB6A zMC`sPRHyJt({*w4N5O73`aRs-)pd0jz~SMEA!q=t(%0q_>O`aaMA>j(pD3&k$VNcb z#-qRS1I%c}#l*aRk4r6_X*FyV)0jOEgA-T;)SlGWv?h2Hh&}F;=MMHrCl6KfDUsfJ{~!P!bgt+hdA9$6#HEF?0Gytf33(Lel$dd-qzO0 zA^k;=@y90(E7i}8wK`t#@0=#bw(WPi`trMyM)Hx=Iz zuE-lGMdO|Mp##?g882&_Fz#2vTjpcwY!0khC9z+0FVl$?HH{ez5RqQ z6#9*5AcZK>M9V4(HTiDq2@-ONk;f!|gm#d8l>U2(i8tXzHZ+iulJXhv+e#C_5*QO3 zOG*Ijh|s2~?^j!O<@t=(k#2(;(hSY9?31>XL-F180`5dcf&eh9lL6 zfsMOd4&H~>*tl)y5oA9n3z@^MSxyL#;!d<+m|bl84rp$E2#iL#%3zHz9ks-nIizB8Wf zbSBgH;qs>In>(kn^%-4eZ9G4ur2L4;R7L%t=9Fgz)(J9f1$YFh&e4ie#AID z7W09AgZ%PjHFj7%+wA;&peUiGuTM=#BWcknlXTFPlvn^CIsJC$LB&mq%0%Xa2iKd2 zo9`WPRNN5&{YI4K2A*K!C2>Y-Ql~%A+QL}Tq{2(?0PB`5;_&8qqb~r2r99cp$k@`} z4xQU9%HUd(a(ZItt&;qI&&Ncmen{q0h+K~h1)5>w)oil4;6iiSlcvlot>v>4$y?i0 z#nw17Dtc2EN-1R*2rDV)LRvv3$I3N+Kjvup@Sy-EsunZ<IiDiqjhojTy;!YQ8w> zr^Ty&|H%;T#(yw2#j=w-`Q{mtUH?3heaety4O%CNg(#h)g9FVU4Vr7XEIDkxuvVM0UY|NQxYxcGYry9g{S^iHr34-Osx zuL;;|QEXvsnzHw$Fhl+>)r-jFK_}aw6a(Rkl=#_G*0sgzD)&b9?97~SV+D=U>!cc& zw@`E>zmHo@QVjgY@elHK0+~FG%i2_-D`eKjC)W>*&TLhu#(%{Xq>HH4`C~v4q9kPZ z&dT;#%P;>TUP!zj)U-)Y&tH!yOlbd!C>+xM5uj+i{l0RZ-pMkrk-wA_k`p&6jNEeTQap|4(po7dRj(a3%+Pm9Rx>m?W zO#WI26b^d!gP(eOPVw}$$NTz{oM^fudi~%%<=ko)2%EmXmB=fYMul6t6)KOn2Qp=E zr6ylLJtuau?Jg`V461{ixvp%Ao@iw&I*6@tb#`fQa`AQ2g^80)IzMS8Z06GxMz^YZ zEn@B6h`-+b8y_6&Mx>jD8WW(Eq1DmT(GA#3*U$$54I}Y};M_$UnIz@G%g-y zt%cVGWlq|4+ComQ3_3NE8b82521ubO0J6XD=)hut67S~BrX}@`*tWQ^@bK~D=+0KXkNFdYWM4l&4RD zU9VKE%y$7sP=N^5tcgx499|bInx3n=b~K-_vV z_lM9#T88EU+ZgPiMvhLL>k=V9jC$4F~Fj{QcLx*&q(Dbu2~CCjHED_!O%Xw{!-< z+VE1aYBah!h=JkiA;#zFkFwORD8(3G_Fhx=ON=;N^nf{h<` z5n4GLy%oUD#g9+rkf|`rxyjOicg@gwlg1)FGZTTuYgewcc65vvMn5$yeERH}$bkdJ zy-mR`7g?%AeID8H#v@I0@3r}+V`$A8I!nVz-kH5Dss>R^n`8bDH#SN`4-WPJ@<VkFKS6Z2#*sqgl6~Q{b(9CC|DkJUr{=)wd>nyl4$~zVGJ;boxgZ7Au;i!kYdW2 ztfSnz7u@+YGE3A1<+A9HhBKvz0Uz_7Zq!k;wB!ZfUwgp_XXxzNFH`-s69^fWKj#jJ zeW(?p_FwMjNnJh9xv`FQSK$5o1se;P8{S zc@4$vtj>>|2ekY{XM4qTC|(I0ZKd0qVX6Fe;X+}3^{)8%c$UCylL~g;W;jkzL?N=| zJ$f_*i3F7(ROQ5AN=wvjUjfYx6#~h~3!JvL<_qsfJS;Ca*EaxCHU+`Sqrf5OMovId zcyfYcuvuBq^jJl{1yk7xxZc6Yd&!}Tic3f!39?W331?Ug|F^f1+%R*HSoB~N`m`tK zrSr9G^K)}CxdA5MU0q#8;&0s|2$!YVy(hG+KWJIkv|VDL2<=JI;Qtu9uROW^4CDHq zySZzbO)=$e#(H4)be8VPJ0iXduE>(Kp;r&vdK9{p~G7qefiv5pg}sQJGa7m)y_4Nz82RHlR+v3yzeSQ=kj6#0wY{2{PW4#$>*9}Z-w za&tepdw2WT8@{d29$MSx_?X>6vK@PkhNk^fXXo?JKknVT2bE{NvNzcb4Gg?8&0s>K z|CYtJ#OM}bkQb}Wt=l+H$0@!31nbms|7r2T4fOQPAu4e~XYcO65S1z>w_-A;dwg^H zE*~1y{9R$)aQknti28NKau>)|waYbaomaU9BI}puEeZDR>Ahx_A+@Dm2OUc%S(`>NM#C z2hLkqz%N``=#PLkh!F)GyD{%!Y%&^hKgfl!h^nlpS^qrou8%Z1289mUp+bC4%;a^E zTsr4K{97~|Z}%yzEjAtm+ z6AwQL!4hcu`FR@jVBmQq@gc$Kmt5T&DU0MR#tfkP#dmPFlg2V-pN@yQ0;Yy+JM*?} zNME!vU{VFHE40g&mPvT(&@R_AGH?|KoaJ?{E9b=;a2G^$9y;>$bSp(#liy#b{qrU9 zvpUAx{P$@z-tJw!0JbI)_US<&-R+=K7|;MFhr`)2va)2)zq@@Xh#q~Q^%7-3EKh*S zZX-n~q=(pQ$AA0)qYLO?=&KF_sGzm|Z|BO?Ol)1<>=xQYOim9T|0W&avpawK{F=6& zUXaPcX*(QGc*e@zNH9hTj2D13{$BQT)B6_20J5=xHOQ#(cP<0IN9bL#^>f>X!zZ3KIjl?}P_IbVl|>jI<(#fm*k>5r~LhK=OA7=z`{e;p=cMAl1G zBYF7WCU4H0nk#yGmLY9lv9@w~x)O!jd$N*>tc`u$WII90O9?&9-NogWbEV+ky>-pa z4Aj&tx8~PlVwPg2=ahg=D%^hw?P|jK#;ZR2f!%EDzoylhPM>aaZti@pJ_N>~Is_|! z^?D~YS~BRkNXAy2No24#nT3r#&>1kKa`h$G(ZA=sqNK<) zgTTa=C&r7ZwgPS;Q?vRlOxPm4TF_R?wzJ((lM&DLmTpqk9LwQ7^(`PHV-!ed&=JJ~ z#y)s80>r;7Z2529zwZU1cPkw-n!pgHc2fL*vjMw5G2>^r#mPsItmDSo2rIF8OpJ)R z$30F<8~>bl1Tpys56+>LdaulFJ0PW?P?4HyUg<~C)YK#qbMDoIYXfB3M9(uJNjQeX>x`7XW{uQTVxw)I!7y(CHbFfEOOit6&G zXUM4YBE`hT<@fe~&PZ@Ypkpfw3T6X!7nhVUGcoDt>PB7sIWi*cHk+=&=P^Bi^zfNt zhutsA{*MWd9Zs?b*upC+yp4?5T7A$HaW1cas?Gf0UgAf>dEm;mYq1$;GIsqL0z%8M zfB)4E(*{ilKr`8L1!Dp&^ZnzOw~POxzQvY}+b4f{dHsXtTEeX-2=suEDsTgQU(J=$ z^mJ;1aqCu9i%R{df1P=FC-oJA5#iHScsZJ(YLSSK0u3U3{>3y5Imz7j9Y%|CnER1r;quvI9k`vlGhpdlA-c+lIe?ukW8S zb@4TI*X_Nqi0VFs zgBT4T@%)v>PyO_dG`@-}R-Vi2Rlt3SRD@4X`4a!ZgC3a50M?QYhgDD&+j0R$fW5hS zstOmvpzvpfTn_;V|CgT_72l;nz7usj<74JG=~Gv?te>1n`5Pqv;KenYrmg$4IWCd|s`&0>e`EqayWCPXIw{vh*V4opWrjKm?))mSegZ#|AJaN=Av$9qI*sKman3JGuE>BD&jX@PJn>1Fn2g5c1v2O`uegmEI2g35Q&f@n~>URWFN zDhz2xmT2F;usg%0SNbUlM6K9~7Q9D*9VRA~8~XV#UOdwmklClQ`yaviEDzQNX)Xmt zgr3(+65bKA?GXyAMO`~fu=oFAHj*SAv{p8gMsyuJA$0CI5`ndnX<9YD?>dF6$v{))BLmmcw$0|I%#z)F42i!(@ZK%szMH$7F$5glPta&?qrZ6OGO zoeLLSF>#1d2;}YoCvO)7g{J`ABWrcT?@JUWWX8tSZg}ouq*lpk{TSW!pTv2={VZd6 zKN7dh)a2l`2sA)ShMhmT0MNrqclhktoyGT&BOxIWmA=9j5gcqP7O%p*JYR}ze0cwV zq;KB%z*vjdUB^bC=hwM^z~JEtKc1!qZ^^WhG=3U54^}H6(k02R?(Ty;JpTG({{7Of zLq4YH=xQ4`fI6eA^=6YHCdfkfChd3}-@O~_Rm6Ax_L?xFgb436-IqJ=T~wj>wy`n4 zun?wI`La**KeO67UAcSFU^S?i$X31KIzsyfEz>qI=v*TcMk?rnogME)HYJgsl?4pd z4SDxNb}S{d;`9xnBx|+*;yp@AerRg4OHR0s2Uw}>k}TEG*AK2!vR>eOw~>ZM231vu zzYNJuy!X0dfL_neuc$^=R#rH-@XUOChy`Ub{NP!J%Z7A5Do2h)?~${3ad6Oa4I)0{ zG2ZB6oDZ&I(@MYci&i+BK|w)~A@PIk?4H8dx5#J{Em-DZY1Ho)|? z$khj25Lx_t9@r~XyS|1X9ZAI`@jENc(z|+ETFNeM_!ay2Gu2KZfc5#)r6ez42!cL{ zOySw*A45ZyF*~*XnDBF8 zEt;exG>nY8KYfzM4kmmO61uRQS17(eg0{v&cet?RGdb21fOS;!B4#aHAp~C{RT7Q4 ziC;x?U6c|vzowxFE)J8O$hAw6@f@`!zl(NaB^60h$e5#e}Gc*X9ifrb@1I*Uq!{YKN=FG7>HYU?`962 zvG~i#<1v1J5;m#5y=kugL3UvgkxBeqzgnE$GN{F_j?-6Ye649Bt!%Ic3m#q$4$ln| zZ_n7eyZ^>Z!I?e4MjugY_}t_R2haSsc&w)8-8iGakngEdu zNK{(tfz*qNP*U?p#N^5|-NL}{5NC!)MELwKLoiN${4}*JVev`&hh&PLE_aqhUN}!&)fii{10E^D@93l3&5!$G|$M&?A^QJ z>Den+uAG6v)3ABxqH~1%ciRDkk}$4~SRB0W^~mRE%xC@?tsY# zOe0`@F_z$Rum{MBSRYo<)z#IQy=lO?z3qL#e(YlO!Fsq5t3Zv|^FR>Q{pa4?2nh?* zwmH>jn4#kagni-tBZRE!sznwIp5LVn`rBg`YRGP1)s6X=QDdK4h;o4a7P0Pdi|vbz zoZOU%Br;3_uZIQ)4+F5Gp{TI_HRqgk6zJyjrE9pS0O4pv)hVe)aEQMaKGyQJKB}&I zU)Z=b9a;*4Scte|UgZbS2^3FK0+(R4wVu}+FeF~-nRsqxXD>$r^xZo(wpz0%T8`a; ze_pnp%fUl@AHcIAuMcz^EC!ej#1hSWaxCWORob}v?3|oEQOJA3 z{-qB9_J^N@6})}>HVXCaN>hA+%Lk-clXb9i*>A-uAUujNlM?cfCcVAazJC3RSv`E{ z7L%gA6NZH1KgOfYCXg_4Yky{!GRcmK#I2P5`WnJEN4zmXiEJgscNsd61wF;m$pTz&S^Do;xt@@Z^3+ItRhGL|VSG|JAB13yAFX>Zqw zU(}LeoQWj|*J8y;kh{BVct66`h`8|ZJcQf>Uj>U4m85;z@sxfbwi+}JYz8DDr_qdH zN@5`(Gx`_f%>WSswn~SiWsQxFvhLWirA&yQzwP74wSj1L%-Q0E!o-HK+LWZg;MjN1 zUQJCc;x6)^W|;iD@~s8}t{P8NcXv0cJwc3Ix_)B3%jx@vsYQ=J$|)l&>xRes-!{s6 zEf-f3N4S zL=PX2l_iP9XssHAg29jebt?92SMe=~XLQxLh{mVi#mee2XaEe%&jtTwGZr;%){~&b z)By&8(Hro9ut+g-u|5|Iw%FFu5g=QotvKEVZkn9T$sU0Vb|wIww@F7Pe1ts9{-Q`Ss~89jQkW! zJ?Y*o5_6+JeqbsO-Ob;mriSz#G(fgmxb5BFt-3My4qrqM}FAXwu zNjWhgDak}rGZ$bmJ7Gw7o%W_u+QzSuURWT8nTK!wVUlf6&xDNoNmtxReVodTQ`7-t~x`2Ly2Whiy^ z?HpUT=Hq@3{7@=N$~I>v0=MUI?%jIChm@wX^DmtL$i_a~_u~#0>z|MDoTBBfk>tC^ zj4Hg8Z;2zfGA`bVeEIng`?9Y7JNpW#gXj-XlGEh|*h>)qVHoZsmGfLe$^Fq=VNz~j zerkPi2=XzHzm5upqzW??qb8zW z!*3*fP!*;ZJ>vT~RRhPY84uv*2}46bqwT!?-@bwU_ZiY+WF4~E23p$Vr%v%oMm-3T z!Af!t2)5H|ZA^2Jx+Ix!u=_36`7nvF=n_>ZwJSB;O2k?v>mHBCgN z3AMqs0D%zE;B}n7~--Zii{xMvrnvrsMyUKq|I@E+wL#3!# zo~}nj!;ZdjA=gCKH8)qtJOo$H4XY&H#^f_)6HPC)Z{dHE``o@$25+0+ByzEzoul-O>l!Bs8L zK!k+W)3H#dblhb8{&~$tdB{7sr4JuHqNJeMr1TUY{k3@=9hDPpVp!XFhySEYDZUGD zRR-UW__(Er#l$uGp#I_EU+4~)mcGSdJ(3(QNGj?AaQqb*8Zl_Z%g|>L_>t=n(|(8h zDX?j9{Yyvy?z8((QBq^fhc3{}u;3EpV^8GU!5$#R1#7w>*@<-W`t=Rlgp6Efh7eQt z4GN-4AEDk2>5t;&ZE;ie7P>SN!(Ba4vw;KAU> zX^i?$flL9Dh()D_S|dG=o($ZGfvUu58_1c9V;%dZ)y^=7;WjRKQ4ia7M^#B^p7O3o zWT6r*0iQw4y(8@cM&!NR&taZhl$JIK(uP@0T)~xm;8XhgBCw1Gxel)exvh(zf4s+O zia*|?QG9nJ&{~nvPkak*IQmM@kEX<<#}}jaAT<@G6%7l4{Fk;}1$Oyd$hKTFEdIHI?b3f|gvuEG4Jv{=nK8X*>XM_jr6T(W>a&>GwBV;Q z+|2d23@t8uU>PZxrGf{5yEC9TsXmyeRw*n^G}uIqvEO^(SyB%%B#5!}9dJ8wRe(r~ ztr92Y-s@N7315w*lL?ZnEG$f0w?deiZ=T*mZq7Gd@S5P0fAD+}omAx98#>7ZIb=*G zC+*EE({OuAp|YIK^hhf=U3Ks=(-_QEo!{Hm9DcdH+b}(A5$3V~!mx zso~?XmD0=)!>{f4SI#QNtsT`O{~^0tk0@yC9Ty}yd1Q?>ouA|j_lng~+@5&-I$7B3 zZ&NN1hWv+`5q3J|b>S7Z7=H||j!P0bqpi)&ZItV^;`U8EpegVEefB^z1%!6=1$Zfi zf>m|zJ}5nzV)OL5nxus1a95?{ai9Tn7G_39QwxjzkQWg(5EmB*vrF&WB`)BIQ;(M( zCzmmCf@V+pj8p4ADM5|?@|ZMdj(u(c5X2Ono`FEED>!2N$}=#QVk*Ld?d<|ohI(v? zOqy90`SehL{SIT?fHyfg38;?0a4#KHSzkp=<7FGl=qKq<1 zOLi!fNF-%uhf+xcnW1GwTXLh&G_o>&$3?kY_wT>w`_D7(rzhicUGMjKp2u;#PS3BB z4}@30n!nm-Gsb`rrgTc~)z#4xCHLhh)}wdroM=4GHB-;V<2*5;nAcyrG{^J@?Mdl%~Vk zCuV((bfs^WV%g#e*%*@?l%TkKz%uwYJiG*$sf^x$IeEj6Qr!aXS?KhrLd3r zE`=~LTxFHTCU=~Q%$mrh*85smNd?g=1Whb`&Bsx%((HTJU3n$~X{WExNC+-lwmy9$ zJY&JO_w{rsJ0I&?KC9>8$+)dU9|eoj zsEA)@%5Mdl1zEqcq`!C?6o*IYbIHe&tYXtJr-8`*3I!!{c1WVg>tudSl*mqt@w3RD zWNwP^voM^znUz%wE2b_0ktkF9C=NguqFx3b!dRSt`7X-ym@*s1RjWwe4?K2g$)u&j z*(6LY-&yWGT_|B}wr~@>ciT(#gKIh2TJKYWvm`onKGHUqNH^Va^!FUvUFeXx{~G4O z57?Ur68+_8JEV=)sjZ*w5dAgryD=E@KfbW>c^(daengoNrc(gwF4WWF$H>{n)INS^ zY7tA5MXiOF_3(bS*Nt45g`bDBy~ugivL+)+<~i<6_g2Ymvf;D77VP&Z%*>f&7GIbY z-Lslz{lcX6Pkd44V9p+RPmEs(^4HN?ILj~?5)%@#1fTUPO3-l+g6`M1Wnsgl6_bPnxV9`C7>gImsyn{v? zvxd<9;|zymMAzW$ee;Xrg)&~NGF=?IOf%b^ckV27ekwiHl`4Z3$zKhS3_3?u+HWz=32`Hn`G^xi^l5{7}8WnJH)Q1ivx-SzYKRG}i zy(|UUWX}SuSuuC91#o~B?eG^yO-8E?XJbBzp9`jvCyyVKH!`MTpIYy%YsxZzhiZNw zM(HT9-40$$PWG>9q{SM6*n3IyfYSjq@R&Hej# z*z7X(9a5*xR~e{R7=Kqr)91CopMRAfvlnwLW`oAY^#~f%U+00+p6t(jccm>x;FLp&PzG?$ET#dbm)J z3rR^&kn0W|LOuw62zXdzH1FEip&?gi=i7&e@=lz12yLakz5TF9kIyFw2q5PNS~s#K z)H=pQjLF&W?AsQw7V1k90uka*PIo8;>`pbIC@cSseud2bUZ$9Cd6X_6J%jIw9}cHLi-w&>UbIC2vw4 z9h6CyR#vZXA7JIq%*p~(W8t|R>pKbOkB5ZttoBiks)ZGGQBKEf1_lQwCyZtEZ|k*~ zIcd^T_YO8(l1sw8t*8izX%0e=I}8*>&@Q3lBaDV;4LOsA(fT3fGXk%tqhl6+50Jqz z{EB0_@n1$IBB`KYZgnrTN{}+ze78F|FtC977k~bIticX+G?tc#QlJmPw@)7*g(o7k zYFnhP2V{|y6k&)fK<&ob@7#F?16Fjaxwa5ika>gG5cB|yMrJQQgp3a}f%C=eg<(&E zDW5Rgu|l>Y8D!voCmy^}6rfOQ-t_|nU^4?@HTd+RfQg8RjHHT!6p2x|%3*G?X|kYu z44ONFz?fW<-@l(q|J;FpNAdEl$7xaE@GWy{!hvanCpEHa8}w1rr`Cv{65!pGxxe~V z0xqRS^@Pr?N)`b;i6u*xzz?$vvUawYi!psY4{%p4qa zb>@D&f+I&2uYmkdcl@qxZ+xJ}2uF6IHbxr& z3pp-U@CVA>`QN_n>|_@pNa;mFY1WZm>!&82F4? zq#&CT4F4*7H4k&})vNs&ECaZ@4ZZt6|Geq5%rdvyw8`1dPV2O&Zw%GwL$vDgF4^0s zz9pK$aLUia`y&)zdV-uW{)-zbtz)J;GMvx6wo%z}W-Q+pf=Ol9MzlfhH(#NltX0;m zz4Jav3?pdV0GvRG5mRiKFTufd!e_=TgCXBi6KIC%qO$jy&ByyCrvX z-4#(hY0V{Hf&2Mxa5jL0J*Bufe%R=AqFK=Dvz_#E|1v}`8c?B)!9`kA_~hZkkN6-u zN*^!gS3VfQ{|>qhs$L8gZT7%|+)EF8MM6SlrKL8oY{_O_JEx|;uA4%>7a>$( zRR2joI7a<<*Ybn7KlNmXv-i-bB9Me4O6uzAn#Yg(ab7)o_z*TvW$=3-JVyiY)-go40;lJTL7WRC>-%kH&k$XNQQW1o88pSL(CY0&y- z&rN(n2+MDmKMRXFIX8F-qUI2^tcJ&racGBygiuWuOEXmWF7O>=f255!1ps;0<;#{K z5Z`-H83G(g*u(SBoI0hk5qwVE5&)BP@h(?TV6%kJ@M zIH4i`)|yqKAn@Kfs zpFh8e7awq2K|w*FB26%AKW2+pOTqxx_wP8%eP$Rx$wEs!UTbHRpnM3E=t}@fzmMX8 z6%bLq8?8o9=ao9|hQzgF$C^p2p~)aR*RKUJBdaw8ONqOFc=?fNmXW!HTrpZ|PEJm| zc%yHD;DM6?*K|RC{@K;OK3-nR-Y;QvfE{-5TG?+xzlu|C@P$VO7Vk7@JaX9?(wLie z4B`Ns8u^4^qk#GFk8E#jNAE%W)monrslr?ey`E!&C-OP?*j1c9k}C2LHSH%)F^&*V z^N_nKeFYZ%8tq+HlPlyWbr@*F+r5YRjjgOek5g0gi;Qf*ln*05jJBXcB3?vKe81Aq z{E_NqHRNLdk?N%)&?Z3B!kGpa2NpP>DZuDZOAq%sa|Lkpu!|iZH{8Dcf~h7Cyb@FL zet`J%I%fT7SSRrB?#J!>T&$x1#{hJtg=z zMg=1)Cu*tRDyrX8YX`r)WyMb2lM93tqEr`W*v}#`jpG*kugc^B3lq~zq}$*b#q=I2F7QDi$#+u%?j(|t_1c{) zmMR0M{7x}vzs%$qE!^EZ9~lIhJGV^ltok?x;g ztKcjF$&U*zo!|Z64?gBP8;lKu|uaoT5{zKgx*|e;!7Z6BPyrGAp z_39Nf0sO>YjB#%KM=kId;kDFS2>F{U!$x*uax0wK@WWy{y8GE>TS_qSt$c*q&Mikx zEAaW;u&A8Dz-v93cG6*FY;)>)?|jX;4g*A zTEfo@U9E8xLhDw34MFqSGo)6+B5UgEd?&_M5=o>#TW~3_QFR0_stFRlBNRXigxuUf zJ{M&)hx0De?+&Q}q?T0U^`?y%@vKleg@kE~{wY43TRV?8$vHRYMZyI6dY1KtKZ6Jf z$ElVuJK6@jY$ zJ1~CT#xJYFtJ|pvfTuezrYtwtS5s4i?E{Y%CkF?Nj}R}OJ$8bL%t491ymQa{_wPee zwS=)>!0%HJyQH;qUl3iF)#8iSGjOUtv@6|4V(8i?=h8mX17f|j1H-PGBg~bRR)wOX zq7({ciPbd*0C>ma{cle^P*)131^-SPPnLlJvjK9_5cc9JuLMzP zLI~n&USpfdg?AT>Kv+tkFSy{W1?s5SR0@nfxZN*ay~-&zLvD9^w&`h{S2ESDCkojg z8z$K`G)vu%d1JRny$6=B>aF9ix)e)xS;HZt!bt2 zgz}P`=+r^Et6*J?@D)I*YqYhu6noCyVZz5zQ^%>HI@%?o_`wHmvOf2;=JJ)cJcTtK z_tvE$iR0|(h!?R9J^+}I6Co0SklI#(_D(EHk1Lp+WG39;eH! z8g82ZD3k2^V`F3Ca6*@WK%dN=F;g)*zdH-|d3$-GgPwiyS3d>mwz}^tgJm(_tF>^; z6$%RC%T7;j^}F=)yz(IqF(8N$bws;??nF(HT)^SX>CTV6E%=4`!kcy0AU!Ck3gOaY zOPne|o*RiR_WriNoICFU*=qZiN#t@*IX!$v`t1GHO6}zFYlj2bWXl#qJCjNx`qHwh zpt}tP=_XP^uIvm9!9#<`)Yl=lh;a^^7atkmU>mQsIm?MO7K7I0k^eb={Cj2A`0jgU z_PnO12IU4?%1^C^>DR7#K;!-~u^A1HBMcYS9IUIXq^*#}MPX+47&<0^t5+AqIl+cG zq=2v%1~;!^5JTznt*wY4=FK~UKQm;~*>^yUx_5a)p`4(cTmv}1_wQeX{~n++NU1Mh z-d@MjD%Rv4fH)J7k1%OWGguZZSg>xg2*Uw5*UXd4+`i@F6D1ihN=jBVGCb6B16_}| z+{65M3bVq&!}EN%-i<8_^6$_0t|+8L7tuSd8Kez%W7JZh^ z{3}DZgFaX{+;SiMC*{5(atTvXPkB{{GIZzZFw}uBty~ zBVEydWpvk&>%jiqGL)a2`w+wIii&oq3|eP_@I!%y@l@Qt{hOo;=mDLD+2qI(P0S?L ztho#GDolx~rV=J#HU1uH*UG#1I?8s2`Cat4;S%2mhF4*m->xj3s^OG>)NnE3t`+6w z*IioRBqW{H@GGe%`hFYStrUDY2nU(`&JFLZhH45KDkBhKl*ik=1H!FsEU?rYH*Q4i zII{DnkxX~UxpKDZULZ40@Gc`AkFxOc*KEtjyup_Av`tkv_X-zDOV1%lKzQt{sqJE@ z_VnMU=EVg3|BD`%1pJshOjuVz_`wzEvgzQDnk+;_p*p~hIV3*`Gd6tCut*~ZT)M=m z6|ng4ewPU@eHq3|ct-%>ew%dfY1|9^P@C4Y{ueGrF}y-X6=Sv(GDv5rNtFIx_2}(* zXi&(c4X26ak|zi*+se;ywH4l*n@i2Y*Fuej8J+CP1D`e%rFd0CsF~^7;}5*{PlV$e zWhq^l`CtMw^IhL9_J5l=b>*~~IK`emP5X2fY)?z9OO$~P6v!C-IFno$<8j=2+QnpK z`ZF@lsCybL|F;N&{*T;U3T0$zkkFKI7$c_T%eh4&6EU(K*?VOK9M=xM3*RHQT3==& zwvR8FnG|sSQs`(Nzf42Gw{eWY-Y4)Q7PD4Re&c<-b#@_Vhh(a7 zPeE{NvI9`pg{7KApun{1_G;2cnz>72-8`T=)L>`aM3$x9C!f?09W{7;_B5fXciZ5 z$#OlUEI=gPx@DCvFgj10U2^mLgi+WNdIokgD_yy-{ z{s=>aBqs~Bb#~g@*tlURilPT=+_Ye~9#@h;8WLI;!kn^)VALc0u$W=~rs(2DFmV1L zr_Xu3M%L3O?c&3iHdgM zSdg7(^ct>gai?Y^%4v08!#kkCpPCnCRB+kv!7eAPmO_F8Q)Uc)4oyys! zBMyt^Yn~mVapOhS&y%hHYb7nwm88HAg5! zYn~F8beOi{W&PcCqa__=3v~CGYaIIMsFz7E3evxP|M_f^{61^4O!-m6yv`RzcqP3M ztH5Ce;bi9&rc)PDRfG}hrFsNzlm=I~vs)Zv<(U!XI zWY#q@!s01Rh~$JKWEEF>bdgB`+;@0^ru3sg`Bg_^uw0VW==Jr2f`y9~9rpAbtUH9! z0GzPTR=?8_R0xH@IK!O@zIYAjl%E3Y1Ka&f<@mKd)&DDj7ygL3K$tDalXahSmc1{* z@>M{4cw}Tt(Lwg=UX(J&5p-)z)fY^em&J_1v|q-TiLsQD2;*S zFK=mSLEsEtEqKOMI3bsH0urm zAF^>{EErBvwIsq8U8$iaENH)MxH~05yraon9J!&vBz2zoe+ehzyCn;ja0G^Xp7e5- zV0H}sQbW88BqO$HZ2L)we(ymnvBP?q(6@ouRb)(C9vvd4ek-0ceYtt=IflS=Inuz1 zFa!20>|4LS8I2VRLbxH7V2w(%_``r1SD~)3-h-=qR@rltf5UTWi_&7WP45)6q=YlD z5~X!@GV=1LD%xr`&j}SX5vNuc*NVzamNmbMFBmL~VJFlb%Ex$A_l5T07GeXO{8{|w zJ$elE^-t-ikseSz>_Y@437%T^=N&q^VL8V1n=oc{G}bnV;NAA{ zsVtice1B<$Zw7(yxg{fdEQWID6MANSWw^40W0|D$M5avnh1}dNn>HQBR+3Re(^Q70 z1DUOcv>KX}k0IGA_?(ZZvnpZk#U6k2Xwgu56Sg(u!^va;H^BnC@1 z%mYsItn7}Y7`duTh0%6f6dyHmo?AOH5Gl{09VO1?KN_yugI*v;j0Ak_9q}yKq~hyK!rbjYGhpT&@`2R}#kQGl!Km z<8i{&Cvv^v+u9R!T#CJ@&b0V#<KUGd{WAy z_D*JKV%5^p0_+MC50iP{@D6LfN;eSOiyBF54_F@KkIiZS$<;ZW#8>~#AWi|p!fpV; z6qYW1|MebD*SYpcaKf*08~Y3nN7pNSSVh;7+Vp+Jl(z~GGJ+0d!n&!6i9~e3w@>bF zbJ_d&m)1*ktAuHGzNXaXBeoR&YF~&ywsIQn$IIDLbajtv`0Kjayw zW#*5W;sXJ4IQH!d8Q=IGIZ)ENNDckDxHwE_LPV$e`K%X) zmNsc9Nrq9IXnv$+>=yV7B_rjk4s{s{PVm*G`{~Xri5JJU3~Fcn{002O!tO(HMDx{nabwQ{yY;=E3syQ^y0f!h z;gP5zjppW0m`>SZtOu2{TrVsPSPV3nLhQJir%PlLwaB|h3)H1jFPL-b7Ce#M7hw7` zM_=9SP5%rjx!ciM!uY0$LRz4Cml(BN2i)Om_Qirc;3rOu^{=}kzUe=K7AbzhC*oBD z@UvuzM>yrK@1fO3mgH|>imu}gpn;uNbSG(QfO`9^icZ<1lZmhS313sR`%d>eAF4bi z;ZyJd6T}sZ%0EH%&x`)`Kn>qWPNouIoiiuqKnrT8+UqTOOr}ooXu}~oft3*QXAPK` z!caVy?n|6X*Z!=pQfRR2uE{l5coEJESg}$lWA@(f9?g&a(`k(>+(Y`_JZmpo8ygtv zbTixvJ#I@MOA*ok28!Zb&kwJeZeJ`8IbW3cVE{Ed#aKx?tDHJc_5 zAKJlD(w%L^H%b-P0$?W|0VQ-;C;=uF4CrvN`P93dIy$i%`x6iL3J2Sccc-ZP91t2k zQ7A?CS7TEY6AW-bnw8d(#TPqqnz!+2x)^WSZ)T~<8jFkT*4@r>9LVqu&h!-ACi~^O z<6Te%0pZwZE~rk3Wjn&v-T9r2yU%c@ltj15NMhvpA+)5#ybzamNtm#@aA4 zw>a0RC6QUEyO4_g5ue6{x`X0W((2(209?*)$ivd~bw;Cq7N-jM%fiLtruMrtnk5Qzm4P*sQD(2NFq7@KkC{_rxLQcThP zYG8j;A;~~4cflz^Ezz{(wZgH4vcFG&1Z1P zq&GdrVX8=liuRkn=&j6jo*}GAKoFR@0&XPGx=1?*sR_{#5U2mBMzQGpMTDwKv(~pi ztw>nD(!4afv(e2y)X}@aaCrZ@l!tf3N3oM=8EVp$4umAANIMX=v8)x8Ij{3^L0q$G zQhoE!w+vDCv{9K1;b19xCm9E0EEMraq_d=LUbMQ zSz=LEUPQPMD!vrZ27WdX(Um+VOj145uWaTuPLF+Zpv9b4+ZvkAp_gq~!WhYtwKpsI z%Sol?09E{JqkmIMAeWJv!yMP-9sDSI_V0gv$JZCtH3}`zs1B7aBubIUsc4I!4{*Rj z4LnRawh^N zPWx<^r9eYjv@>$G*}LrHam|B4zD<#HQn?pu36{P&&uO8v^bogv*$P|)JnpvK`O?QW zCv2Rj1^Fp@SwqeHfR1Df6*Eg|mJD-)3u>J%`@gLMo$$0cAl+D%jqYD8s=K3uwgvLg zQl*<*<%jggHN6|R4zU)5;j5C*l6zH3pgtZZHZT?HCo!0Z35|!Pq3~o`{X>#wUu%ca zM|Q+Zoka^N%oFN@=0p`y`359|UI;+|3`+A~#opvGgoYg74mM!tXdyEf4cn18ycy?+3Irn6P#h>7|`O8usLjWEc_Wm1mUQAs-MaMn>_VFk|dWpDBC1fd}kXSIBU zWL`mLPDsEG#(uy|L)!JD(&=9E8aDYyZfA)=fe-@!G z%Sw$Pr)c4;{!LSW9XOGx350(B!t}9wiBT1ZAlP9K8z>@Jkyph5~UftJ?^Z&{{Gpug3Z}!-HWv`VDG`W5#m3F5g2r# z=B(r^g5eE;KfdL=;5?bl%6hasik6F`8X1EgXG;+_hGw435QenOr1>nJ-dN&5et0l! ziA7=m#ud2au)Cy`n(Im}ezIA_W>ONe%X@P?i_0utpcLU8jqLP|JIuI{vLY-!md$i^0I%&y*W{&G=ezOeW9uLU`n z^W0;;`;Iuq_@cqB{?3&vrc1c%V8{e541uzglmH5YspL7NU>H_I&Usx~DJmnwDwY+U z)}1ML;CAlKOP6HAssdzWX$TGuj@Q1E-pkMB?kI55P%J~J1#}2Xf9?JIB3B3kwgS7w z(F?-l2gsKZC0An{yIv2k_%{N{}yM+nTaw!4fa&yoA0ye6p$bXhffdO}a9 zIcCe9gt%T(2K_dSTPmPJ9adAxB}+uPLYuNI=0E${3`s>y6Xh69?3+cyo1ad9x$BNP zWa17r6bw+;^Yh)^+^`l*iy2?-6%0#_jT+|7_4P@*2jg>=7@D21)X~X!nT-t!;4&c) z6NjCKz-GI(X&l`A&`AQ39!1A1@vrP-Mc-$@vqokz$E32s8&!KG%$F_Zo|7K449JzJ z=|mIx24V5*pW7EDq78%ZV*xY-9TZ@=+&D+n1g8 zJ;PAB;>MWPinhR+O^Fp#n*65d$XPK|EFs&qNAG$!8KP8X+9$?2%J2g|&?NA&hS5K4 zJq|%?5=%`_n6F=7W@lm&iHPQ+L@!fpzCx-S*eDK;JriSx#y+pKg_?-3f|N7D(a470 zfmyxK*rx-B{ebBk$b#e_PYuW=-S@C<>utn45vcg9!O`z^+1ZNsv=XB zb``&g<)`>*PV4iVb@5$HCy$#Z`ih37ivPIcEuPQAqvvy&31;^e5&%Hj1!s~4ef#hF zVIsp^flV+%S5;d;Am13MYP?&7+yRXFslQ9r?IF&dJ3JnPcAAyBqc%!!f-z2eqc4P@SMkDvi(T}np&E2{5cF! zQL#-EL`ib`2@w^QaTITWywV16lQm(^d=_9?FJ)#T#OIH(!#G7v$eYZ4eF;IMlG?56?2}u6gUJ{mAhE;#q9T z#+hkp%MggcKY570p>69gabr1=S7+0!c|~@~^Gs^6RH+ zimq9^U~!|9G|44^74MCri@p7<71FP#0%|83(@mKyG#}CpD+jg@A87T4lNCST+Ss96 zyi4~Twu^9Ym~oMHu2m(06mAG8q0%tI!IkluSloj^lwe}rXSjOwhO@UgIb}C8W@%r} zD%O!2VgbsO&y<~rSbjLRRPWT=ukL71rDx-dkrqO%rdnTaZV3HUub?L3u)jG@ej3|NpCQ2!Hoq@ z&G33UUJs1>m!iYEkEG}jXV()g8d3ptAECX*PE)lhZ9WU3`}@4NR(Jb; zCVW#)xjVMiVuKfARvx?tQX3JsI3F7;9;u71G`LHd=mRoM%ubw5QE8Hf7m9`uH?^8Y z;NYsLtNV=46{nAfRmy3wW^u0z*S5Jqy*ypgVIUQd+jN*~PivF$F#T)?5xR&%L$G&x zhS1$j>Q3+3HU4m%H?FZ-(qWmIrqsq_r{-)3|n>e8VkZk+&HNWS2rP@6U8mdbv$W3{LF3lwO;O4jHlp`9}CH-$sy~Gzu-OgZv(d zpVUSTxP*J{4n>BEPfKDhTE^OE_*K;1KDNF$g zJq50Ry1Q2bPuCX-csY37fGcu54pC&mgQ67gW<=?6Mpor#H; zj#^Hh{;eCfr@NdqowxgS(b8YK3*7Y6G*#W99Y>IZZ^!-h z=F2$vrS80q)!tk*;E>sboz4hHea z?Mo&EyiVR_rK{<&X-r65+yPD_nwQMDYU*DY`JC2 z3EOAD+7JZ%a+F|O3i{f?OTObwJ}xdU>1Nm#DI`{no9A2RJk9v;y9x6fPr-yo33fzI zJuVK@a+1h8Uaw59m_w zkS5c&V>7pGw0>wG1xu?#e2hz#Qm5I!?3FQnOjpH|MMjw;c6kmpsy#Swt2WF;&mfOF@e7Cl58HaZo?>1UF55{lbzD@5$dkQEW&R!f0DoPtZ z4=((xL90-5!GnRWQ{d6HPjQSe7h0pEGjuX+v@0EenQ6QajIYsk-e6aUx6iD^f-5Pg zx_MH*!LEK|{ia8#p1>i%lRRfs43jY64oDg=WaEjEI^Q{OK4)QohDUsNYe{@{d+*2? z3~3XuT&02cQS;fD=~hb+XoXSl;xxt!Ko?JctW0mu9ukE^ej$zI6!|YkZ>EEN o z7s}F=MhgFy!@mkg7l=66hE><*2H4rDg=?V?&8{F1KQIpQ1+bn#$?XjhY>Q@y@B^!o zxdNp$s(HlUk$2gRx(YB~7`Mv@|FDErw7V5)h;t!?gor-sPuvvcSZ~K#aT(-b%O%{L zb(it2YWe@h{po_;M&;`otfoA2qyqMY!%hsuv;j6sKvAKV>W54DR2G!=-_}CvXla$^0KN|03ti%r+X~nGo3=JsjH&iQIDmsg zl8X__Zh4y)GDo3CAJ7?%eWR_{WlxN?TgS5e`t< zqcJ6nn6C3R;V?*0<{=_TBstgX3!j(@gl6HHC#fbQBcY7O!!eKaZWrqj#N}RGdn!s< zjhl|maoTzXg#a}HW(ZSM(*dz6DXG6@AZOjy^L}ia7|HuvCAUX3D*4CA(R7)N&;(PF z47_-F>F6z72i8e*o#Hqvvm57SPgF%hIy11xl2SBiP zfKPA}#-^qL?FZrYH@WlsA{LOhmpe6OV&+terd}khDijepcm4@8n1jo5U>cfo;@3Wm z7&#z(15562T1&FAv5|Omn2zPRmWS# zQl5VjIV&zBYDK8Q4ulF3>lY4fvGVazmD`UU3Yudw>U$kBx&9)V39~+x?1a*bEr-|^ zw$huEXF7afAlaAD=xL5s2!A4hBk#0}lfR0V*P{B}gQ)|qLX7xbd|9a`9EtTCFiOaa zh|maZ@Z5RoHlbUnKqO7$AH*xBs@9q#6D=D3$6J620;xa5l5NHp5plotogdDcoSY?G zglAaExDo=FB9l%>5-pclBqng7!V>Y7RaN!D!G!3Q+whg_Y({~dQv+Z)%ogdNT5WbA zEv>3^54M4d^#^KCWSgEv@&TVUs3SdK>S7O)_4Qpmh7~xv#=x~iBDu0Kno*hTNo04w zP>40}P)W<&C0_9(>0sg-f9>Tttz}I&g9M$nV&i);EF3t!0Z%~YistJl1L-qsUG7XJ z5(MeS=O#bmX}jdJZrwV{O86$w1;L}2-NcqGhL03GmbZW_Bnoc>6@di1x2GxNzp2l~ zy@x>4!!kCpQ=suCTj#=ngom)UAy234{rfwI`h!yl(7>U+ez;nPM4f}GrWfF}$Weem zkiBg7DNKEAY%T+a5}W!(K)ujAdHXXd1ib>Scd{w&cDAumtG4Kj%08}C$kvs%cCc!m zH73sRJ(s55JCKf59&inT)mS@~f&TFEV+eJqi&gWZXEW6Pz!Nbs+}vT>#Yzkfi(w!f z#;*^H8%nU@3?CV^5e!%`CWWnHuv<^!%fP@GfHp{1^6~MJ@*n*8oR;`?=RHA>}=kMI8dR*65wlkYxQ(uSx z4x5{qiORPQTDGu=2ni28X?F1BEg5X9Cl_;qW=W;Yab?aM6b)1?|G$x zkC+-o7a)`Tif)8-mC48>WKb}(=mhx?FEFO-SQ~=>@c!-FHC)CXkF)pRg{cMO3V2u! zSF-=$Uo~Bw5HVN#+HL1KhMydrCq{u@s>q1J%*QV<2`#jPuOHh$mG+fu&zIj@E`Jlo zhd#?+opwwF7!YZ2!f6{o$tDqn|qJG%0>^2MRnvTGx9kS%m<5bs4FSGc=Raz;STH=#)FOB zCyl*zKgp}IJOU@_Cj_1#C#JV6dfL0Upm({kN9S2U6c$lu18H*)RA zO+8VjuS79T?mO@nL(J~;<_%J*T+uQ-&@$dk1t=k7mfIB`5R2c0Q|vyqzhA8#9Nc*u z^HXl!iu2;uUXh!!)O2VYoNLT<7xMDLy+}s>6pU-^&eK`77apCNOn3s)> z_19sOM0{(@5ys#2-IRv;J zEDk#~PiyssKB+)yo^6D>+?&ZaQ(U7>cYMatG-9U`4}zD8jS9yK2@fWrU}1+P4F1^8 zb4F+^K^9$%U=!_kShX=TIv$3Hv;OD*bz=2fIR%SO6OAD3ks|c8jkNMKYy$ot>d_$3 literal 0 HcmV?d00001 diff --git a/README.md b/README.md index e4ace1b..e329fbe 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ This repository contains the source code for the main Android App, which is published on (Google Play Store)[https://play.google.com/store/apps/details?id=uk.org.openseizuredetector]. +For a detailed architectural overview (activities, service, data flow, resources) see APP_STRUCTURE.md. + This seizure detector uses a Garmin smart watch to collect movement (acceleration) and heart rate data which is used to detect tonic-clonic epileptic seizures. See (the OpenSeizureDetector Web Site)[https://www.openseizuredetector.org.uk/] for more details.