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:

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๏ƒ

Use SDK::Sensor::Connection:

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.

See SensorLayerMessages.hpp.

Full Example๏ƒ

See tutorial Service.hpp/cpp for multi-sensor handling with GUI comm.