From bf61cbe72922045ff850dbdbabfaece8c7a38177 Mon Sep 17 00:00:00 2001 From: TobiasRoeddiger Date: Sun, 4 Oct 2026 22:35:31 +0200 Subject: [PATCH] feat(imu): define lossless compact BLE motion sample layout --- generated/c/CMakeLists.txt | 1 + generated/c/include/imu_protocol.h | 44 +++++++++++ generated/c/src/imu_protocol.c | 73 +++++++++++++++++++ generated/dart/CHANGELOG.md | 1 + .../dart/lib/open_earable_protocols.dart | 1 + generated/dart/lib/src/imu_protocol.dart | 61 ++++++++++++++++ schemas/imu/README.md | 38 ++++++++++ schemas/imu/protocol.yml | 26 +++++++ tests/test_imu_codec.py | 43 +++++++++++ 9 files changed, 288 insertions(+) create mode 100644 generated/c/include/imu_protocol.h create mode 100644 generated/c/src/imu_protocol.c create mode 100644 generated/dart/lib/src/imu_protocol.dart create mode 100644 schemas/imu/README.md create mode 100644 schemas/imu/protocol.yml create mode 100644 tests/test_imu_codec.py diff --git a/generated/c/CMakeLists.txt b/generated/c/CMakeLists.txt index ccc129d..d0241c3 100644 --- a/generated/c/CMakeLists.txt +++ b/generated/c/CMakeLists.txt @@ -8,6 +8,7 @@ add_library(open_earable_protocols STATIC "src/audio_configuration_protocol.c" "src/audio_response_protocol.c" "src/button_protocol.c" + "src/imu_protocol.c" "src/led_protocol.c" "src/ppg_protocol.c" "src/wireless_audio_configuration_protocol.c" diff --git a/generated/c/include/imu_protocol.h b/generated/c/include/imu_protocol.h new file mode 100644 index 0000000..7dbe739 --- /dev/null +++ b/generated/c/include/imu_protocol.h @@ -0,0 +1,44 @@ +// Generated from schemas/imu/protocol.yml. Do not edit by hand. +#pragma once + +#include "protocol_runtime.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * BLE only from firmware 2.3.0; six signed raw motion readings followed by three unchanged + * compensated magnetometer floats. See README for exact scales and firmware selection. + */ +typedef struct imu_compact_sample_t imu_compact_sample_t; +struct imu_compact_sample_t { + int16_t accel_x; + int16_t accel_y; + int16_t accel_z; + int16_t gyro_x; + int16_t gyro_y; + int16_t gyro_z; + float mag_x; + float mag_y; + float mag_z; +}; + + +/** + * Encode a binary representation of this message. BLE only from firmware 2.3.0; six signed + * raw motion readings followed by three unchanged compensated magnetometer floats. See + * README for exact scales and firmware selection. + */ +protocol_status_t imu_compact_sample_encode(const imu_compact_sample_t *message, uint8_t *buffer, size_t buffer_size, size_t *bytes_written); +/** + * Decode a binary representation into this message. BLE only from firmware 2.3.0; six + * signed raw motion readings followed by three unchanged compensated magnetometer floats. + * See README for exact scales and firmware selection. + */ +protocol_status_t imu_compact_sample_decode(imu_compact_sample_t *message, const uint8_t *buffer, size_t buffer_size, size_t *bytes_read); + +#ifdef __cplusplus +} +#endif + diff --git a/generated/c/src/imu_protocol.c b/generated/c/src/imu_protocol.c new file mode 100644 index 0000000..0d830af --- /dev/null +++ b/generated/c/src/imu_protocol.c @@ -0,0 +1,73 @@ +// Generated from schemas/imu/protocol.yml. Do not edit by hand. +#include "imu_protocol.h" + +static protocol_status_t imu_compact_sample_write(protocol_writer_t *writer, const imu_compact_sample_t *message) { + protocol_status_t status; + status = protocol_write_int16(writer, message->accel_x); + if (status != PROTOCOL_OK) return status; + status = protocol_write_int16(writer, message->accel_y); + if (status != PROTOCOL_OK) return status; + status = protocol_write_int16(writer, message->accel_z); + if (status != PROTOCOL_OK) return status; + status = protocol_write_int16(writer, message->gyro_x); + if (status != PROTOCOL_OK) return status; + status = protocol_write_int16(writer, message->gyro_y); + if (status != PROTOCOL_OK) return status; + status = protocol_write_int16(writer, message->gyro_z); + if (status != PROTOCOL_OK) return status; + status = protocol_write_float(writer, message->mag_x); + if (status != PROTOCOL_OK) return status; + status = protocol_write_float(writer, message->mag_y); + if (status != PROTOCOL_OK) return status; + status = protocol_write_float(writer, message->mag_z); + if (status != PROTOCOL_OK) return status; + return PROTOCOL_OK; +} + +static protocol_status_t imu_compact_sample_read(protocol_reader_t *reader, imu_compact_sample_t *message) { + protocol_status_t status; + status = protocol_read_int16(reader, &message->accel_x); + if (status != PROTOCOL_OK) return status; + status = protocol_read_int16(reader, &message->accel_y); + if (status != PROTOCOL_OK) return status; + status = protocol_read_int16(reader, &message->accel_z); + if (status != PROTOCOL_OK) return status; + status = protocol_read_int16(reader, &message->gyro_x); + if (status != PROTOCOL_OK) return status; + status = protocol_read_int16(reader, &message->gyro_y); + if (status != PROTOCOL_OK) return status; + status = protocol_read_int16(reader, &message->gyro_z); + if (status != PROTOCOL_OK) return status; + status = protocol_read_float(reader, &message->mag_x); + if (status != PROTOCOL_OK) return status; + status = protocol_read_float(reader, &message->mag_y); + if (status != PROTOCOL_OK) return status; + status = protocol_read_float(reader, &message->mag_z); + if (status != PROTOCOL_OK) return status; + return PROTOCOL_OK; +} + +protocol_status_t imu_compact_sample_encode(const imu_compact_sample_t *message, uint8_t *buffer, size_t buffer_size, size_t *bytes_written) { + protocol_writer_t writer = { buffer, buffer_size, 0 }; + protocol_status_t status = imu_compact_sample_write(&writer, message); + if (status != PROTOCOL_OK) { + return status; + } + if (bytes_written != NULL) { + *bytes_written = writer.offset; + } + return PROTOCOL_OK; +} + +protocol_status_t imu_compact_sample_decode(imu_compact_sample_t *message, const uint8_t *buffer, size_t buffer_size, size_t *bytes_read) { + protocol_reader_t reader = { buffer, buffer_size, 0 }; + protocol_status_t status = imu_compact_sample_read(&reader, message); + if (status != PROTOCOL_OK) { + return status; + } + if (bytes_read != NULL) { + *bytes_read = reader.offset; + } + return PROTOCOL_OK; +} + diff --git a/generated/dart/CHANGELOG.md b/generated/dart/CHANGELOG.md index 08db9f2..581706e 100644 --- a/generated/dart/CHANGELOG.md +++ b/generated/dart/CHANGELOG.md @@ -1,5 +1,6 @@ ## 0.0.4 +- Define 24-byte BLE IMU samples for firmware 2.3.x; retain float values/units and the 36-byte SD layout. - Define lossless 10-byte BLE PPG samples for firmware 2.3.x alongside the unchanged 16-byte legacy/SD layout. ## 0.0.3 diff --git a/generated/dart/lib/open_earable_protocols.dart b/generated/dart/lib/open_earable_protocols.dart index 58b1fee..6b8f93e 100644 --- a/generated/dart/lib/open_earable_protocols.dart +++ b/generated/dart/lib/open_earable_protocols.dart @@ -10,6 +10,7 @@ export 'src/protocol_runtime.dart' export 'src/audio_configuration_protocol.dart'; export 'src/audio_response_protocol.dart'; export 'src/button_protocol.dart'; +export 'src/imu_protocol.dart'; export 'src/led_protocol.dart'; export 'src/ppg_protocol.dart'; export 'src/wireless_audio_configuration_protocol.dart'; diff --git a/generated/dart/lib/src/imu_protocol.dart b/generated/dart/lib/src/imu_protocol.dart new file mode 100644 index 0000000..c3e0287 --- /dev/null +++ b/generated/dart/lib/src/imu_protocol.dart @@ -0,0 +1,61 @@ +// Generated from schemas/imu/protocol.yml. Do not edit by hand. +import 'dart:typed_data'; +import 'protocol_runtime.dart'; + +/// BLE only from firmware 2.3.0; six signed raw motion readings followed by three unchanged +/// compensated magnetometer floats. See README for exact scales and firmware selection. +class ImuCompactSample { + /// Creates a ImuCompactSample value. + ImuCompactSample({required this.accel_x, required this.accel_y, required this.accel_z, required this.gyro_x, required this.gyro_y, required this.gyro_z, required this.mag_x, required this.mag_y, required this.mag_z}); + + final int accel_x; + final int accel_y; + final int accel_z; + final int gyro_x; + final int gyro_y; + final int gyro_z; + final double mag_x; + final double mag_y; + final double mag_z; + + /// Decodes a complete ImuCompactSample value from [bytes]. + factory ImuCompactSample.fromBytes(Uint8List bytes) { + final reader = ProtocolReader(bytes); + final value = ImuCompactSample._read(reader); + reader.finish(); + return value; + } + + static ImuCompactSample _read(ProtocolReader reader) { + final accel_x = reader.int16(); + final accel_y = reader.int16(); + final accel_z = reader.int16(); + final gyro_x = reader.int16(); + final gyro_y = reader.int16(); + final gyro_z = reader.int16(); + final mag_x = reader.float32(); + final mag_y = reader.float32(); + final mag_z = reader.float32(); + return ImuCompactSample(accel_x: accel_x, accel_y: accel_y, accel_z: accel_z, gyro_x: gyro_x, gyro_y: gyro_y, gyro_z: gyro_z, mag_x: mag_x, mag_y: mag_y, mag_z: mag_z); + } + + /// Encodes this value to the protocol binary representation. + Uint8List toBytes() { + final writer = ProtocolWriter(); + _write(writer); + return writer.takeBytes(); + } + + void _write(ProtocolWriter writer) { + writer.int16(accel_x); + writer.int16(accel_y); + writer.int16(accel_z); + writer.int16(gyro_x); + writer.int16(gyro_y); + writer.int16(gyro_z); + writer.float32(mag_x); + writer.float32(mag_y); + writer.float32(mag_z); + } +} + diff --git a/schemas/imu/README.md b/schemas/imu/README.md new file mode 100644 index 0000000..cfdbf50 --- /dev/null +++ b/schemas/imu/README.md @@ -0,0 +1,38 @@ +# IMU sensor data + +Keep sensor ID **0**, the existing sensor-stream UUIDs, and the existing packet +header/timestamps. Read each device's firmware version before subscribing: +firmware 2.1.x/2.2.x sends nine little-endian float32 values (36 bytes/sample); +firmware 2.3.x, including prereleases, sends the 24-byte sample below. Select +per connection and refresh after reconnect/FOTA; do not infer from packet size. + +| Byte offsets | Wire value | +|---|---| +| 0–5 | Accelerometer X/Y/Z, three little-endian signed int16 counts | +| 6–11 | Gyroscope X/Y/Z, three little-endian signed int16 counts | +| 12–23 | Magnetometer X/Y/Z, three unchanged little-endian float32 values in µT | + +Restore the six motion values to float32 before applying the existing parsing +scheme or exposing values to applications: + +- Acceleration in m/s²: `float32(raw * 0.0005985504249110818)`. + This is exactly the firmware's float32 `(2.0f * 9.80665f) / 32768.0f` scale. +- Angular velocity in degrees/s: `float32(raw * 0.06103515625)`. + This is exactly `2000.0f / 32768.0f`. + +These scales are part of the 2.3.x wire contract, matching the existing ±2 g +and ±2000 degrees/s sensor configuration. Recover counts by rounding the +existing float divided by its scale; reject values outside int16 or values +that do not reconstruct the original float32 exactly. Do not round, rescale, +or remove magnetometer values: they already include factory compensation. + +The advertised parsing scheme, library values/types/units and phone CSVs stay +unchanged. **All SD/.oe IMU samples remain nine float32 values (36 bytes)**, +including recordings from 2.3.x. File readers must use the legacy layout. + +The packet envelope is unchanged: sensor ID (uint8), payload byte length +(uint8), first timestamp (uint64 microseconds), samples, then a uint16 period +in microseconds for multi-sample packets only. All integers are little-endian. +A 244-byte notification holds nine compact samples (228 bytes), versus six +legacy samples (228 bytes). Sample payload shrinks by one third; actual BLE +throughput still depends on radio scheduling and batching. diff --git a/schemas/imu/protocol.yml b/schemas/imu/protocol.yml new file mode 100644 index 0000000..8105953 --- /dev/null +++ b/schemas/imu/protocol.yml @@ -0,0 +1,26 @@ +protocol: imu +version: 1 +description: BLE IMU samples for firmware 2.3.x. Logical values and all SD records remain nine float32 values. + +messages: + compact_sample: + description: BLE only from firmware 2.3.0; six signed raw motion readings followed by three unchanged compensated magnetometer floats. See README for exact scales and firmware selection. + fields: + - name: accel_x + type: int16 + - name: accel_y + type: int16 + - name: accel_z + type: int16 + - name: gyro_x + type: int16 + - name: gyro_y + type: int16 + - name: gyro_z + type: int16 + - name: mag_x + type: float + - name: mag_y + type: float + - name: mag_z + type: float diff --git a/tests/test_imu_codec.py b/tests/test_imu_codec.py new file mode 100644 index 0000000..5c8208e --- /dev/null +++ b/tests/test_imu_codec.py @@ -0,0 +1,43 @@ +"""Verify signed IMU fields and unchanged float32 magnetometer bytes.""" +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +class ImuCodecTest(unittest.TestCase): + def test_c_wire_layout(self): + cc = shutil.which('cc') + if cc is None: + self.skipTest('C compiler unavailable') + source = r''' +#include +#include +#include "imu_protocol.h" +int main(void) { + const uint8_t expected[24] = { + 0,128,255,255,0,0,1,0,0,64,255,127, + 0,0,160,63,0,0,32,192,0,0,0,128}; + imu_compact_sample_t value = {-32768,-1,0,1,16384,32767,1.25f,-2.5f,-0.0f}; + uint8_t out[24]; size_t size; + assert(imu_compact_sample_encode(&value, out, sizeof(out), &size) == PROTOCOL_OK); + assert(size == 24 && memcmp(out, expected, sizeof(out)) == 0); + imu_compact_sample_t decoded; + assert(imu_compact_sample_decode(&decoded, out, sizeof(out), &size) == PROTOCOL_OK); + assert(decoded.accel_x == -32768 && decoded.accel_y == -1 && decoded.accel_z == 0); + assert(decoded.gyro_x == 1 && decoded.gyro_y == 16384 && decoded.gyro_z == 32767); + assert(memcmp(&decoded.mag_x, &value.mag_x, 3 * sizeof(float)) == 0); + for (size_t n = 0; n < 24; ++n) + assert(imu_compact_sample_decode(&decoded, out, n, &size) != PROTOCOL_OK); +} +''' + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path / 'test.c').write_text(source) + subprocess.run([cc, '-std=c99', '-Wall', '-Wextra', '-Werror', + '-Igenerated/c/include', str(path / 'test.c'), + 'generated/c/src/imu_protocol.c', + 'generated/c/src/protocol_runtime.c', + '-o', str(path / 'test')], check=True) + subprocess.run([str(path / 'test')], check=True)