# Sensor Layer ## Overview The SensorLayer provides a high-level C++ API for subscribing to and receiving data from various sensors via IPC messages through the SDK Kernel. It abstracts low-level driver interactions, allowing applications to connect to sensors by type, specify sampling periods and latency, and receive batched sensor data events. Key components: - [`SDK::Sensor::Connection`](../Libs/Header/SDK/SensorLayer/SensorConnection.hpp): Manages subscription (RequestDefault) and connection (RequestConnect/Disconnect). - Sensor data delivery via [`SDK::Message::Sensor::EventData`](../Libs/Header/SDK/Messages/SensorLayerMessages.hpp) messages. - Data processing with [`SDK::Sensor::DataBatch`](../Libs/Header/SDK/SensorLayer/SensorDataBatch.hpp), [`SDK::Sensor::DataView`](../Libs/Header/SDK/SensorLayer/SensorDataView.hpp), and type-specific parsers in DataParsers/. ## Sensor Types All available sensor types are defined in [`SDK::Sensor::Type`](../Libs/Header/SDK/SensorLayer/SensorTypes.hpp): | Category | Type | Hex Value | Description | Parser Available | Fields | |----------|------|-----------|-------------|------------------|--------| | IMU | ACCELEROMETER | 0x10 | Acceleration (3-axis) | Yes | X,Y,Z (float m/s^2) - 3 | | IMU | ACCELEROMETER_RAW | 0x11 | Acceleration raw | Yes | X,Y,Z (int16 raw) - 3 | | IMU | GYROSCOPE | 0x20 | Angular rate (3-axis) | Yes | X,Y,Z (float deg/s) - 3 | | IMU | GYROSCOPE_RAW | 0x21 | Angular rate raw | Yes | X,Y,Z (int16 raw) - 3 | | IMU | MAGNETIC_FIELD | 0x30 | Magnetic field; corrected only when MAG_CALIBRATED | Yes | X,Y,Z (float uT), MAG_CALIBRATED (u32) - 4 | | IMU | MAGNETIC_FIELD_RAW | 0x31 | Magnetic field, as measured | Yes | X,Y,Z (float uT) - 3 | | Cardio | HEART_BEAT | 0x40 | Beat peak event | No | - | | Cardio | HEART_RATE | 0x41 | Current heart rate (bpm) | Yes | BPM (float), TRUST_LEVEL (float) - 2 | | Cardio | HEART_RATE_METRICS | 0x42 | Aggregated metrics (AHR, RHR) | Yes | AHR (float bpm), RHR (float bpm) - 2 | | Pedometer | STEP_DETECTOR | 0x50 | Step event | Yes | STEP_DETECTED (u32=1) - 1 | | Pedometer | STEP_COUNTER | 0x51 | Step count since reboot | Yes | STEP_COUNT (u32) - 1 | | Pedometer | FLOOR_COUNTER | 0x60 | Floor counter | Yes | FLOORS_UP (i32), FLOORS_DOWN (i32) - 2 | | Ambient | AMBIENT_TEMPERATURE | 0x70 | Ambient temperature | Yes | TEMP (float °C?) - 1 | | Ambient | PRESSURE | 0x80 | Atmospheric pressure | Yes | PRESS (float Pa), PRESS_SEA_LEVEL (float Pa) - 2 | | Ambient | ALTIMETER | 0x90 | Altimeter | Yes | ALTITUDE (float m) - 1 | | Wrist | WRIST_MOTION | 0xA0 | Wrist-motion event | Yes | WRIST_MOTION (u32=1) - 1 | | Motion | MOTION_DETECT | 0xB0 | Motion states (NO_MOTION, MOTION, SIG_MOTION) | Yes | ID (u32 enum) - 1 | | Motion | ACTIVITY_RECOGNITION | 0xC0 | Activity classification (STILL, WALKING, RUNNING, UNKNOWN) | Yes | ID (u32 enum), CONFIDENCE (u8 %) - 2 | | Motion | GESTURE_RECOGNITION | 0xD0 | Discrete gesture events | No | - | | Daily | ACTIVITY | 0xE0 | Active minutes (minutes) | Yes | DURATION (u32 ms) - 1 | | Optical | PPG | 0xF0 | Photoplethysmogram | No | - | | Optical | SPO2 | 0xF1 | Blood-oxygen saturation (SpO2) | Yes | SATURATION (float %), TRUST_LEVEL (float) - 2 | | Optical | ECG | 0x100 | Electrocardiogram | No | - | | GPS | GPS_LOCATION | 0x110 | GNSS location | Yes | PRECISION (f m), COORDS_VALID (bool), LAT (f deg), LON (f deg), ALT (f m) - 5 | | GPS | GPS_SPEED | 0x111 | GNSS speed | Yes | SPEED (f m/s) - 1 | | GPS | GPS_DISTANCE | 0x112 | GNSS distance/odometer | Yes | DISTANCE (f m) - 1 | | Battery | BATTERY_LEVEL | 0x120 | Charge level (%) | Yes | LEVEL (f 0-100) - 1 | | Battery | BATTERY_CHARGING | 0x121 | Charging state | Yes | CONNECTED (u32 bool), CHARGING (u32 bool) - 2 | | Battery | BATTERY_METRICS | 0x122 | Voltage/current/capacity | Yes | VOLTAGE (f V), CURRENT (f mA), AVG_CURRENT (f mA), CAPACITY (f mAh), DESIGN_CAPACITY (f mAh) - 5 | | Fusion | FUSION | 0x130 | Accelerometer + gyroscope samples | Yes | ACCEL_X,Y,Z (float m/s^2, gravity included) + GYRO_X,Y,Z (float deg/s) - 6 | | Fusion | FUSION_RAW | 0x131 | Raw fusion inputs | Yes | ACCEL_X,Y,Z + GYRO_X,Y,Z (int16 raw) - 6 | | Touch | TOUCH_DETECT | 0x140 | Touch/worn/unworn | Yes | TOUCH (u32 bool) - 1 | ## Connection API Use [`SDK::Sensor::Connection`](../Libs/Header/SDK/SensorLayer/SensorConnection.hpp): Constructors: - `Connection(Type id, float period = 0, uint32_t latency = 0)`: Lazy resolve handle on connect. - `Connection(uint8_t handle, float period = 0, uint32_t latency = 0)`: Use existing handle. Methods: - `bool connect()`: Subscribe if needed, connect with stored params. - `bool connect(float period, uint32_t latency)`: Update params, connect. - `bool isValid()`: Handle != 0. - `bool isConnected()`: Active connection. - `void disconnect()`: Send RequestDisconnect. - `bool matchesDriver(uint16_t handle)`: Compare handles. Destructor auto-disconnects. Implementation in [SensorConnection.cpp](../Libs/Source/SensorLayer/SensorConnection.cpp). ## Data Reception & Processing **Reception**: Poll kernel IPC: ```cpp SDK::Kernel& kernel = SDK::KernelProviderService::GetInstance().getKernel(); SDK::MessageBase* msg; while (kernel.comm.getMessage(msg, 1000)) { if (msg->getType() == SDK::MessageType::EVENT_SENSOR_LAYER_DATA) { SDK::Message::Sensor::EventData* event = static_cast(msg); SDK::Sensor::DataBatch batch(event->data, event->count, event->stride); processBatch(event->handle, batch); } kernel.comm.releaseMessage(msg); } ``` **Processing**: - `DataBatch`: Iterable views (`batch.size()`, `batch[i]`). - `DataView`: Timestamps (`getTimestamp()`, `getTimestampUs()`), fields (`f[0]`, `u[0]`, `i[0]` float/u32/i32). See [SensorData.hpp](../Libs/Header/SDK/SensorLayer/SensorData.hpp), [DataView.hpp](../Libs/Header/SDK/SensorLayer/SensorDataView.hpp). ## Sensors Detailed usage for each supported sensor (with parser). For others, use DataView directly. ### ACCELEROMETER (0x10) **Parser**: `SDK::SensorDataParser::Accelerometer` **Fields**: | Index | Name | Type | Unit | |-------|------|------|------| | 0 | X | float | m/s^2 | | 1 | Y | float | m/s^2 | | 2 | Z | float | m/s^2 | The values include gravity: a watch lying still reads about 9.81 on the vertical axis. Divide by 9.80665 to get g. **Code Snippet**: ```cpp SDK::Sensor::Connection conn(SDK::Sensor::Type::ACCELEROMETER, 0.1f); conn.connect(); void processBatch(uint16_t handle, SDK::Sensor::DataBatch& batch) { if (conn.matchesDriver(handle)) { for (uint16_t i = 0; i < batch.size(); ++i) { SDK::SensorDataParser::Accelerometer p(batch[i]); if (p.isDataValid()) { float x = p.getX(), y = p.getY(), z = p.getZ(); uint64_t ts = p.getTimestampUs(); // Use data } } } } ``` ### ACCELEROMETER_RAW (0x11) **Parser**: `SDK::SensorDataParser::AccelerometerRaw` **Fields**: X/Y/Z (int16_t raw) **Code Snippet**: Similar, `int16_t x = p.getX();` etc. ### MAGNETIC_FIELD (0x30) **Parser**: `SDK::SensorDataParser::MagneticField` **Fields**: | Index | Name | Type | Unit | |-------|------|------|------| | 0 | MAG_X | float | uT | | 1 | MAG_Y | float | uT | | 2 | MAG_Z | float | uT | | 3 | MAG_CALIBRATED | u32 | 1 when a correction was applied | The field is reported in the watch's own axes with whatever hard- and soft-iron correction is in force. A watch that has never been calibrated still produces samples, and `MAG_CALIBRATED` is how you tell: they carry the part's own offsets, which is what a calibration is worked out from and not something to take a direction from. The parser also derives a compass bearing, since that is a function of one sample and nothing else. It is the bearing of 12 o'clock - on the wrist, the direction the forearm points - in degrees clockwise from **magnetic** north, with no declination applied, and `getAzimuthDeg()` means nothing unless `isAzimuthValid()`. **Code Snippet**: ```cpp SDK::Sensor::Connection conn(SDK::Sensor::Type::MAGNETIC_FIELD, 0.1f); conn.connect(); void processBatch(uint16_t handle, SDK::Sensor::DataBatch& batch) { if (conn.matchesDriver(handle)) { for (uint16_t i = 0; i < batch.size(); ++i) { SDK::SensorDataParser::MagneticField p(batch[i]); if (p.isAzimuthValid()) { float bearing = p.getAzimuthDeg(); // Level only. See below for a tilted watch. } } } } ``` `getAzimuthDeg()` assumes the watch is held roughly level. Tilting it brings part of the vertical field into the watch's own plane, and where the field is steep that part is the larger one: in the UK a quarter of the way up is enough to turn the answer round. To compensate for how the watch is actually being held, pass gravity in - read it from the accelerometer wherever you already read it: ```cpp float bearing = 0.0f; if (p.getAzimuthDegTilted(ax, ay, az, bearing)) { // Same arithmetic, so a level watch reads the same either way. } ``` `ax, ay, az` are the X, Y, Z of an `ACCELEROMETER` sample taken close in time to this one, passed as they are. That is the accelerometer's own reading, not a vector pointing down: a watch lying face-up at rest reads Z positive. Passed the other way round, the bearing comes out mirrored, and nothing can catch that, because face-down is a real attitude. The tilted bearing stays on 12 o'clock however the watch is held: with the arm raised or lowered, the wrist turned about the forearm, both at once, the face on edge or face down. The one attitude it refuses is 12 o'clock pointing within about 10 degrees of straight up or down, where the forearm has no direction on the map. Whether a rose can still be read off a face tilted that far is a separate question, and one for whatever draws it. The arithmetic behind both bearings is public, for a field that did not arrive in one of these samples - one from your own fusion, say: `MagneticField::levelProject()`, `hasDirection()` and `bearingDeg()` are static and take plain values. ### MAGNETIC_FIELD_RAW (0x31) **Parser**: `SDK::SensorDataParser::MagneticFieldRaw` **Fields**: X/Y/Z (float uT), the field exactly as measured No correction is applied and no bearing is offered: a direction taken from an uncorrected field is wrong by however far the part's own offsets push it. This is what a calibration is derived from, and what shows how large those offsets are. ### HEART_RATE (0x41) **Parser**: `SDK::SensorDataParser::HeartRate` **Fields**: | Index | Name | Type | Unit | |-------|------|------|------| | 0 | BPM | float | bpm | | 1 | TRUST_LEVEL | float | - | **Code Snippet**: As above, `float bpm = p.getBpm(); float trust = p.getTrustLevel();` ### HEART_RATE_METRICS (0x42) **Parser**: `SDK::SensorDataParser::HeartRateMetrics` **Fields**: | Index | Name | Type | Unit | |-------|------|------|------| | 0 | AHR | float | bpm | | 1 | RHR | float | bpm | ### STEP_DETECTOR (0x50) **Parser**: `SDK::SensorDataParser::StepDetector` **Fields**: STEP_DETECTED (u32 =1) **Code**: `if (p.isStepDetected()) { /* step */ }` ### STEP_COUNTER (0x51) **Parser**: `SDK::SensorDataParser::StepCounter` **Fields**: STEP_COUNT (u32) ### FLOOR_COUNTER (0x60) **Parser**: `SDK::SensorDataParser::FloorCounter` **Fields**: FLOORS_UP (i32), FLOORS_DOWN (i32) ### AMBIENT_TEMPERATURE (0x70) **Parser**: `SDK::SensorDataParser::Temperature` **Fields**: TEMP (float) ### PRESSURE (0x80) **Parser**: `SDK::SensorDataParser::Pressure` **Fields**: PRESS (float Pa), PRESS_SEA_LEVEL (float Pa) ### ALTIMETER (0x90) **Parser**: `SDK::SensorDataParser::Altimeter` **Fields**: ALTITUDE (float m) ### WRIST_MOTION (0xA0) **Parser**: `SDK::SensorDataParser::WristMotion` **Fields**: WRIST_MOTION (u32=1) `if (p.isWristMotion()) { /* raise wrist */ }` ### MOTION_DETECT (0xB0) **Parser**: `SDK::SensorDataParser::MotionDetect` **Fields**: ID (u32: NO_MOTION=0, MOTION, SIG_MOTION) `MotionDetect::Motion m = p.getID();` ### ACTIVITY_RECOGNITION (0xC0) **Parser**: `SDK::SensorDataParser::ActivityRecognition` **Fields**: ID (STILL=0, WALKING, RUNNING, UNKNOWN), CONFIDENCE (u8 %) ### ACTIVITY (0xE0) **Parser**: `SDK::SensorDataParser::Activity` **Fields**: DURATION (u32 ms) ### SPO2 (0xF1) **Parser**: `SDK::SensorDataParser::Spo2` Blood-oxygen saturation derived from the optical PPG path. Delivered as a processed scalar with a trust level, mirroring HEART_RATE. **Fields**: | Index | Name | Type | Unit | |-------|------|------|------| | 0 | SATURATION | float | % | | 1 | TRUST_LEVEL | float | - | `float spo2 = p.getSaturation(); float trust = p.getTrustLevel();` ### GPS_LOCATION (0x110) **Parser**: `SDK::SensorDataParser::GpsLocation` **Fields**: PRECISION (f m), COORDS_VALID (bool), LAT/LON (f deg), ALT (f m) `p.getLatitude()`, `p.isCoordinatesValid()` ### GPS_SPEED (0x111) **Parser**: `SDK::SensorDataParser::GpsSpeed` **Fields**: SPEED (f m/s) ### GPS_DISTANCE (0x112) **Parser**: `SDK::SensorDataParser::GpsDistance` **Fields**: DISTANCE (f m) ### BATTERY_LEVEL (0x120) **Parser**: `SDK::SensorDataParser::BatteryLevel` **Fields**: LEVEL (f 0-100 %) ### BATTERY_CHARGING (0x121) **Parser**: `SDK::SensorDataParser::BatteryCharging` **Fields**: CONNECTED (bool), CHARGING (bool) ### BATTERY_METRICS (0x122) **Parser**: `SDK::SensorDataParser::BatteryMetrics` **Fields**: VOLTAGE (f V), CURRENT (f mA), AVERAGE_CURRENT (f mA), CAPACITY (f mAh), DESIGN_CAPACITY (f mAh) ### TOUCH_DETECT (0x140) **Parser**: `SDK::SensorDataParser::Touch` **Fields**: TOUCH (bool) For a sensor without a parser, read its fields through DataView, by the layout its driver documents: ```cpp SDK::Sensor::DataView view = batch[0]; float value = view.f[0]; // index and type come from that layout ``` ## Workflow Diagram ```mermaid graph TD A[Connection(Type, period)] --> B[conn.connect()] B --> C[Kernel: EventData(handle, data[], count, stride)] C --> D[app.getMessage() -> EventData] D --> E[DataBatch batch] E --> F[DataView view = batch[0]] F --> G[Parser p(view)] G --> H{isDataValid?} H -->|Yes| I[Process fields] ``` ## Example: Multi-Sensor Service Adapted from tutorial [`Service.cpp`](Tutorials/Sensors/Software/Libs/Sources/Service.cpp): ```cpp // In app service loop SDK::Sensor::Connection hr(SDK::Sensor::Type::HEART_RATE); hr.connect(); // Auto-subscribe & connect while (true) { SDK::MessageBase* msg; if (kernel.comm.getMessage(msg, 1000)) { if (msg->getType() == SDK::MessageType::EVENT_SENSOR_LAYER_DATA) { auto* event = static_cast(msg); SDK::Sensor::DataBatch batch(event->data, event->count, event->stride); if (hr.matchesDriver(event->handle)) { auto parser = SDK::SensorDataParser::HeartRate(batch[0]); if (parser.isDataValid()) { float bpm = parser.getBpm(); // Send to GUI or process } } } kernel.comm.releaseMessage(msg); } } hr.disconnect(); ``` Full tutorial in Docs/Tutorials/Sensors/. ## Additional Messages - RequestList: List handles for type. - RequestGetDesc: Sensor descriptor string. See [`SensorLayerMessages.hpp`](../Libs/Header/SDK/Messages/SensorLayerMessages.hpp). ## Full Example See tutorial [Service.hpp/cpp](Tutorials/Sensors/Software/Libs/Header/Service.hpp) for multi-sensor handling with GUI comm.