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: Manages subscription (RequestDefault) and connection (RequestConnect/Disconnect).Sensor data delivery via
SDK::Message::Sensor::EventDatamessages.Data processing with
SDK::Sensor::DataBatch,SDK::Sensor::DataView, and type-specific parsers in DataParsers/.
Sensor Types๏
All available sensor types are defined in SDK::Sensor::Type:
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๏
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.
Data Reception & Processing๏
Reception: Poll kernel IPC:
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<SDK::Message::Sensor::EventData*>(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, DataView.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:
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:
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:
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:
SDK::Sensor::DataView view = batch[0];
float value = view.f[0]; // index and type come from that layout
Workflow Diagram๏
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:
// 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<SDK::Message::Sensor::EventData*>(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.
Full Example๏
See tutorial Service.hpp/cpp for multi-sensor handling with GUI comm.