From 4e62203d1453198caea51e7503eb59d9ae6bdc0d Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:29:33 +0300 Subject: [PATCH 01/12] lib/src/managers/tau_sensor_handler.dart: implemented structure of tau sensor handler --- lib/src/managers/tau_sensor_handler.dart | 71 ++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 lib/src/managers/tau_sensor_handler.dart diff --git a/lib/src/managers/tau_sensor_handler.dart b/lib/src/managers/tau_sensor_handler.dart new file mode 100644 index 0000000..76f9c19 --- /dev/null +++ b/lib/src/managers/tau_sensor_handler.dart @@ -0,0 +1,71 @@ +import 'dart:async'; +import 'dart:typed_data'; + +import '../../open_earable_flutter.dart'; +import '../constants.dart'; +import 'sensor_handler.dart'; +import '../utils/sensor_value_parser/sensor_value_parser.dart'; + +class TauSensorHandler extends SensorHandler { + final DiscoveredDevice _discoveredDevice; + final BleGattManager _bleManager; + + final SensorValueParser _sensorValueParser; + + TauSensorHandler({ + required DiscoveredDevice discoveredDevice, + required BleGattManager bleManager, + required SensorValueParser sensorValueParser, + }) : _discoveredDevice = discoveredDevice, + _bleManager = bleManager, + _sensorValueParser = sensorValueParser; + + @override + Stream> subscribeToSensorData(int sensorId) { + if (!_bleManager.isConnected(_discoveredDevice.id)) { + throw Exception("Can't subscribe to sensor data. Earable not connected"); + } + + StreamController> streamController = + StreamController(); + _bleManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: sensorServiceUuid, + characteristicId: sensorDataCharacteristicUuid, + ) + .listen( + (data) async { + if (data.isNotEmpty && data[2] == sensorId) { + Map parsedData = await _parseData(data); + streamController.add(parsedData); + } + }, + onError: (error) { + logger.e("Error while subscribing to sensor data: $error"); + }, + ); + + return streamController.stream; + } + + @override + Future writeSensorConfig(TauSensorConfig sensorConfig) async { + //TODO: implement + throw UnimplementedError(); + } + + /// Parses raw sensor data bytes into a [Map] of sensor values. + Future> _parseData(data) async { + ByteData byteData = ByteData.sublistView(Uint8List.fromList(data)); + + return _sensorValueParser.parse(byteData, []); + } +} + +class TauSensorConfig extends SensorConfig { + //TODO: implement + Uint8List toBytes() { + throw UnimplementedError(); + } +} From 48ca5a7794fbf32db157799bebd58d9e15bae7a7 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:30:37 +0300 Subject: [PATCH 02/12] lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart: implemented tau ring sensor --- .../tau_ring/tau_ring_sensor.dart | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart diff --git a/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart b/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart new file mode 100644 index 0000000..5cc8e5e --- /dev/null +++ b/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart @@ -0,0 +1,59 @@ +import 'dart:async'; + +import '../../../../managers/sensor_handler.dart'; +import '../../sensor.dart'; + +class TauRingSensor extends Sensor { + const TauRingSensor({ + required this.sensorId, + required super.sensorName, + required super.chartTitle, + required super.shortChartTitle, + required List axisNames, + required List axisUnits, + required this.sensorHandler, + super.relatedConfigurations = const [], + }) : _axisNames = axisNames, _axisUnits = axisUnits; + + final int sensorId; + final List _axisNames; + final List _axisUnits; + + final SensorHandler sensorHandler; + + @override + List get axisNames => _axisNames; + + @override + List get axisUnits => _axisUnits; + + @override + int get axisCount => _axisNames.length; + + @override + Stream get sensorStream { + StreamController streamController = StreamController(); + sensorHandler.subscribeToSensorData(sensorId).listen( + (data) { + int timestamp = data["timestamp"]; + + List values = []; + for (var entry in (data as Map).entries) { + if (entry.key == 'units') { + continue; + } + + values.add(entry.value.toDouble()); + } + + SensorIntValue sensorValue = SensorIntValue( + values: values, + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }, + ); + return streamController.stream; + } +} From 616d60be7c87c0ee09d6a4e49045a432c119ccba Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Thu, 16 Oct 2025 13:31:24 +0300 Subject: [PATCH 03/12] lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart: implemented imu data parsing --- .../tau_ring_value_parser.dart | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart diff --git a/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart new file mode 100644 index 0000000..8fe9ddc --- /dev/null +++ b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart @@ -0,0 +1,59 @@ +import 'dart:typed_data'; + +import '../sensor_scheme_parser/sensor_scheme_reader.dart'; +import 'sensor_value_parser.dart'; + +class TauRingValueParser extends SensorValueParser { + @override + Map parse(ByteData data, List sensorSchemes) { + int framePrefix = data.getUint8(0); + if (framePrefix != 0x00) { + throw Exception("Invalid frame prefix: $framePrefix"); //TODO: use specific exception + } + + int sequenceNum = data.getUint8(1); + int cmd = data.getUint8(2); + int subOpcode = data.getUint8(3); + int status = data.getUint8(4); + ByteData payload = ByteData.sublistView(data, 5); + + Map parsedData = { + "sequenceNum": sequenceNum, + "cmd": cmd, + "subOpcode": subOpcode, + "status": status, + }; + + switch (cmd) { + case 0x40: // IMU + switch (subOpcode) { + case 0x01: // Accel only + Map accelData = _parseImuComp(payload); + parsedData['ACC'] = accelData; + break; + case 0x06: // Accel + Gyro + Map accelData = _parseImuComp(ByteData.sublistView(payload, 0, 5)); + Map gyroData = _parseImuComp(ByteData.sublistView(payload, 6)); + parsedData['ACC'] = accelData; + parsedData['GYRO'] = gyroData; + break; + default: + throw Exception("Unknown sub-opcode for sensor data: $subOpcode"); + } + default: + throw Exception("Unknown command: $cmd"); + } + + return parsedData; + } + + Map _parseImuComp(ByteData data) { + Map parsedComp = {}; + + parsedComp['X'] = data.getInt16(0, Endian.little); + parsedComp['Y'] = data.getInt16(2, Endian.little); + parsedComp['Z'] = data.getInt16(4, Endian.little); + + return parsedComp; + } +} From 48db247976889b2d45be0a83c2d04fbe0a23af45 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:35:33 +0300 Subject: [PATCH 04/12] lib/src/managers/tau_sensor_handler.dart: implemented sensor configuration and listening to sensor data --- lib/src/managers/tau_sensor_handler.dart | 42 +++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/lib/src/managers/tau_sensor_handler.dart b/lib/src/managers/tau_sensor_handler.dart index 76f9c19..f13d504 100644 --- a/lib/src/managers/tau_sensor_handler.dart +++ b/lib/src/managers/tau_sensor_handler.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:open_earable_flutter/src/models/devices/tau_ring.dart'; + import '../../open_earable_flutter.dart'; -import '../constants.dart'; import 'sensor_handler.dart'; import '../utils/sensor_value_parser/sensor_value_parser.dart'; @@ -31,10 +32,9 @@ class TauSensorHandler extends SensorHandler { _bleManager .subscribe( deviceId: _discoveredDevice.id, - serviceId: sensorServiceUuid, - characteristicId: sensorDataCharacteristicUuid, - ) - .listen( + serviceId: TauRingGatt.service, + characteristicId: TauRingGatt.rxChar, + ).listen( (data) async { if (data.isNotEmpty && data[2] == sensorId) { Map parsedData = await _parseData(data); @@ -51,8 +51,18 @@ class TauSensorHandler extends SensorHandler { @override Future writeSensorConfig(TauSensorConfig sensorConfig) async { - //TODO: implement - throw UnimplementedError(); + if (!_bleManager.isConnected(_discoveredDevice.id)) { + Exception("Can't write sensor config. Earable not connected"); + } + + Uint8List sensorConfigBytes = sensorConfig.toBytes(); + + await _bleManager.write( + deviceId: _discoveredDevice.id, + serviceId: TauRingGatt.service, + characteristicId: TauRingGatt.txChar, + byteData: sensorConfigBytes, + ); } /// Parses raw sensor data bytes into a [Map] of sensor values. @@ -64,8 +74,22 @@ class TauSensorHandler extends SensorHandler { } class TauSensorConfig extends SensorConfig { - //TODO: implement + int cmd; + int subOpcode; + + TauSensorConfig({ + required this.cmd, + required this.subOpcode, + }); + Uint8List toBytes() { - throw UnimplementedError(); + int randomByte = DateTime.now().microsecondsSinceEpoch & 0xFF; + + return Uint8List.fromList([ + 0x00, + randomByte, + cmd, + subOpcode, + ]); } } From 1622fce6d8b11ace511d3f12b82303647d154c0a Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:39:11 +0300 Subject: [PATCH 05/12] lib/src/modles/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart: use parsed sensor data correctly --- .../sensor_specializations/tau_ring/tau_ring_sensor.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart b/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart index 5cc8e5e..7f918a4 100644 --- a/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart +++ b/lib/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart @@ -38,12 +38,12 @@ class TauRingSensor extends Sensor { int timestamp = data["timestamp"]; List values = []; - for (var entry in (data as Map).entries) { + for (var entry in (data[sensorName] as Map).entries) { if (entry.key == 'units') { continue; } - values.add(entry.value.toDouble()); + values.add(entry.value); } SensorIntValue sensorValue = SensorIntValue( From 2e6d4d8c3ab0cf72762bbfe21b68e8bf820c7be4 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:40:08 +0300 Subject: [PATCH 06/12] lib/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart: implemented primitive sensor configuration --- .../tau_ring_sensor_configuration.dart | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 lib/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart diff --git a/lib/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart b/lib/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart new file mode 100644 index 0000000..4867e56 --- /dev/null +++ b/lib/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart @@ -0,0 +1,37 @@ +import 'package:open_earable_flutter/src/managers/tau_sensor_handler.dart'; + +import '../sensor_configuration.dart'; + +class TauRingSensorConfiguration extends SensorConfiguration { + + final TauSensorHandler _sensorHandler; + + TauRingSensorConfiguration({required super.name, required super.values, required TauSensorHandler sensorHandler}) + : _sensorHandler = sensorHandler; + + @override + void setConfiguration(TauRingSensorConfigurationValue value) { + TauSensorConfig config = TauSensorConfig( + cmd: value.cmd, + subOpcode: value.subOpcode, + ); + + _sensorHandler.writeSensorConfig(config); + } +} + +class TauRingSensorConfigurationValue extends SensorConfigurationValue { + final int cmd; + final int subOpcode; + + TauRingSensorConfigurationValue({ + required super.key, + required this.cmd, + required this.subOpcode, + }); + + @override + String toString() { + return key; + } +} From bb93098ce89ba6e0102c45cf08382775ddf6e04a Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:40:53 +0300 Subject: [PATCH 07/12] lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart: parse batched sensor data --- .../tau_ring_value_parser.dart | 51 ++++++++++++++++--- 1 file changed, 44 insertions(+), 7 deletions(-) diff --git a/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart index 8fe9ddc..526aa3e 100644 --- a/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart +++ b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart @@ -6,36 +6,41 @@ import 'sensor_value_parser.dart'; class TauRingValueParser extends SensorValueParser { @override Map parse(ByteData data, List sensorSchemes) { + int baseTs = DateTime.now().millisecondsSinceEpoch; + int framePrefix = data.getUint8(0); if (framePrefix != 0x00) { throw Exception("Invalid frame prefix: $framePrefix"); //TODO: use specific exception } + if (data.lengthInBytes < 5) { + throw Exception("Data too short to parse"); //TODO: use specific exception + } + int sequenceNum = data.getUint8(1); int cmd = data.getUint8(2); int subOpcode = data.getUint8(3); int status = data.getUint8(4); ByteData payload = ByteData.sublistView(data, 5); - Map parsedData = { + Map dataHeader = { + "timestamp": baseTs, "sequenceNum": sequenceNum, "cmd": cmd, "subOpcode": subOpcode, "status": status, }; + final List> parsedData; + switch (cmd) { case 0x40: // IMU switch (subOpcode) { case 0x01: // Accel only - Map accelData = _parseImuComp(payload); - parsedData['ACC'] = accelData; + parsedData = _parseAccel(payload); break; case 0x06: // Accel + Gyro - Map accelData = _parseImuComp(ByteData.sublistView(payload, 0, 5)); - Map gyroData = _parseImuComp(ByteData.sublistView(payload, 6)); - parsedData['ACC'] = accelData; - parsedData['GYRO'] = gyroData; + parsedData = _parseAccelGyro(payload); break; default: throw Exception("Unknown sub-opcode for sensor data: $subOpcode"); @@ -44,6 +49,38 @@ class TauRingValueParser extends SensorValueParser { throw Exception("Unknown command: $cmd"); } + return parsedData.map((m) => m..addAll(dataHeader)).toList().first; //TODO: return full list + } + + List> _parseAccel(ByteData data) { + if (data.lengthInBytes % 6 != 0) { + throw Exception("Invalid data length for Accel: ${data.lengthInBytes}"); + } + List> parsedData = []; + for (int i = 0; i < data.lengthInBytes; i += 6) { + if (i + 6 > data.lengthInBytes) break; + ByteData sample = ByteData.sublistView(data, i, i + 6); + Map accelData = _parseImuComp(sample); + parsedData.add({'Accelerometer': accelData}); + } + return parsedData; + } + + List> _parseAccelGyro(ByteData data) { + if (data.lengthInBytes % 12 != 0) { + throw Exception("Invalid data length for Accel+Gyro: ${data.lengthInBytes}"); + } + List> parsedData = []; + for (int i = 0; i < data.lengthInBytes; i += 12) { + if (i + 12 > data.lengthInBytes) break; + ByteData sample = ByteData.sublistView(data, i, i + 12); + Map accelData = _parseImuComp(ByteData.sublistView(sample, 0, 6)); + Map gyroData = _parseImuComp(ByteData.sublistView(sample, 6)); + parsedData.add({ + 'Accelerometer': accelData, + 'Gyroscope': gyroData, + }); + } return parsedData; } From 908f2d04eb264bc2d0ab712397008c4e42ce8af1 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:41:26 +0300 Subject: [PATCH 08/12] lib/src/models/devices/tau_ring.dart: implemented tau ring device --- lib/src/models/devices/tau_ring.dart | 68 ++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 lib/src/models/devices/tau_ring.dart diff --git a/lib/src/models/devices/tau_ring.dart b/lib/src/models/devices/tau_ring.dart new file mode 100644 index 0000000..0823ac7 --- /dev/null +++ b/lib/src/models/devices/tau_ring.dart @@ -0,0 +1,68 @@ +import '../../../open_earable_flutter.dart'; + + +/// τ-Ring integration for OpenEarable. +/// Implements Wearable (mandatory) + SensorManager (exposes sensors). +class TauRing extends Wearable implements SensorManager, SensorConfigurationManager { + TauRing({ + required DiscoveredDevice discoveredDevice, + required this.deviceId, + required super.name, + List sensors = const [], + List sensorConfigs = const [], + required BleGattManager bleManager, + required super.disconnectNotifier, + }) : _sensors = sensors, + _sensorConfigs = sensorConfigs, + _bleManager = bleManager, + _discoveredDevice = discoveredDevice; + + final DiscoveredDevice _discoveredDevice; + + final List _sensors; + final List _sensorConfigs; + final BleGattManager _bleManager; + + @override + final String deviceId; + + @override + List> get sensorConfigurations => _sensorConfigs; + @override + List> get sensors => _sensors; + + @override + Future disconnect() { + return _bleManager.disconnect(_discoveredDevice.id); + } + + @override + Stream, SensorConfigurationValue>> get sensorConfigurationStream => const Stream.empty(); +} + +// τ-Ring GATT constants (from the vendor AAR) +class TauRingGatt { + static const String service = 'bae80001-4f05-4503-8e65-3af1f7329d1f'; + static const String txChar = 'bae80010-4f05-4503-8e65-3af1f7329d1f'; // write + static const String rxChar = 'bae80011-4f05-4503-8e65-3af1f7329d1f'; // notify + + // opcodes (subset) + static const int cmdApp = 0xA0; // APP_* handshake + static const int cmdVers = 0x11; // version + static const int cmdBatt = 0x12; // battery + static const int cmdSys = 0x37; // system (reset etc.) + static const int cmdPPGQ2 = 0x32; // start/stop PPG Q2 + + // build a framed command: [0x00, rnd, cmdId, payload...] + static List frame(int cmd, {List payload = const [], int? rnd}) { + final r = rnd ?? DateTime.now().microsecondsSinceEpoch & 0xFF; + return [0x00, r & 0xFF, cmd, ...payload]; + } + + static List le64(int ms) { + final b = List.filled(8, 0); + var v = ms; + for (var i = 0; i < 8; i++) { b[i] = v & 0xFF; v >>= 8; } + return b; + } +} From 637471ff5c619cebc1833b7ef4b218990e1ea4ec Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:42:04 +0300 Subject: [PATCH 09/12] lib/src/models/devices/tau_ring_factory.dart: implemented tau ring factory --- lib/src/models/devices/tau_ring_factory.dart | 77 ++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 lib/src/models/devices/tau_ring_factory.dart diff --git a/lib/src/models/devices/tau_ring_factory.dart b/lib/src/models/devices/tau_ring_factory.dart new file mode 100644 index 0000000..9b12719 --- /dev/null +++ b/lib/src/models/devices/tau_ring_factory.dart @@ -0,0 +1,77 @@ +import 'package:open_earable_flutter/src/models/capabilities/sensor_configuration_specializations/tau_ring_sensor_configuration.dart'; +import 'package:open_earable_flutter/src/models/capabilities/sensor_specializations/tau_ring/tau_ring_sensor.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../../managers/tau_sensor_handler.dart'; +import '../../utils/sensor_value_parser/tau_ring_value_parser.dart'; +import '../capabilities/sensor.dart'; +import '../capabilities/sensor_configuration.dart'; +import '../wearable_factory.dart'; +import 'discovered_device.dart'; +import 'tau_ring.dart'; +import 'wearable.dart'; + +class TauRingFactory extends WearableFactory { + @override + Future createFromDevice(DiscoveredDevice device, {Set options = const {}}) { + if (bleManager == null) { + throw Exception("Can't create τ-Ring instance: bleManager not set in factory"); + } + if (disconnectNotifier == null) { + throw Exception("Can't create τ-Ring instance: disconnectNotifier not set in factory"); + } + + final sensorHandler = TauSensorHandler( + discoveredDevice: device, + bleManager: bleManager!, + sensorValueParser: TauRingValueParser(), + ); + + List sensorConfigs = [ + TauRingSensorConfiguration( + name: "6-Axis IMU", + values: [ + TauRingSensorConfigurationValue(key: "On", cmd: 0x40, subOpcode: 0x06), + TauRingSensorConfigurationValue(key: "Off", cmd: 0x40, subOpcode: 0x00), + ], + sensorHandler: sensorHandler, + ), + ]; + List sensors = [ + TauRingSensor( + sensorId: 0x40, + sensorName: "Accelerometer", + chartTitle: "Accelerometer", + shortChartTitle: "Accel", + axisNames: ["X", "Y", "Z"], + axisUnits: ["g", "g", "g"], + sensorHandler: sensorHandler, + ), + TauRingSensor( + sensorId: 0x40, + sensorName: "Gyroscope", + chartTitle: "Gyroscope", + shortChartTitle: "Gyro", + axisNames: ["X", "Y", "Z"], + axisUnits: ["dps", "dps", "dps"], + sensorHandler: sensorHandler, + ), + ]; + + final w = TauRing( + discoveredDevice: device, + deviceId: device.id, + name: device.name, + sensors: sensors, + sensorConfigs: sensorConfigs, + disconnectNotifier: disconnectNotifier!, + bleManager: bleManager!, + ); + return Future.value(w); + } + + @override + Future matches(DiscoveredDevice device, List services) async { + return services.any((s) => s.uuid.toLowerCase() == TauRingGatt.service); + } +} From a0e60812ac99dea3337f4c9952d23e92cf77d90f Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Fri, 17 Oct 2025 09:42:19 +0300 Subject: [PATCH 10/12] lib/open_earable_flutter.dart: use tau ring factory --- lib/open_earable_flutter.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index 673bb53..0527fe5 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -19,6 +19,7 @@ import 'src/managers/wearable_disconnect_notifier.dart'; import 'src/models/capabilities/stereo_device.dart'; import 'src/models/capabilities/system_device.dart'; import 'src/models/devices/discovered_device.dart'; +import 'src/models/devices/tau_ring_factory.dart'; import 'src/models/devices/wearable.dart'; export 'src/models/devices/discovered_device.dart'; @@ -106,6 +107,7 @@ class WearableManager { CosinussOneFactory(), PolarFactory(), DevKitFactory(), + TauRingFactory(), ]; factory WearableManager() { From 6aaf65e7301a3bdafd1abba5f359ab1328ed8245 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:15:05 +0100 Subject: [PATCH 11/12] lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart: handle buffered data --- .../tau_ring_value_parser.dart | 146 ++++++++++++------ 1 file changed, 102 insertions(+), 44 deletions(-) diff --git a/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart index 526aa3e..f5588c8 100644 --- a/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart +++ b/lib/src/utils/sensor_value_parser/tau_ring_value_parser.dart @@ -1,96 +1,154 @@ import 'dart:typed_data'; +import '../../../open_earable_flutter.dart' show logger; import '../sensor_scheme_parser/sensor_scheme_reader.dart'; import 'sensor_value_parser.dart'; class TauRingValueParser extends SensorValueParser { + // 100 Hz → 10 ms per sample + static const int _samplePeriodMs = 10; + + int _lastSeq = -1; + int _lastTs = 0; + @override - Map parse(ByteData data, List sensorSchemes) { - int baseTs = DateTime.now().millisecondsSinceEpoch; + List> parse( + ByteData data, + List sensorSchemes, + ) { + + + logger.t("Received Tau Ring sensor data: size: ${data.lengthInBytes} ${data.buffer.asUint8List()}"); + - int framePrefix = data.getUint8(0); + final int framePrefix = data.getUint8(0); if (framePrefix != 0x00) { - throw Exception("Invalid frame prefix: $framePrefix"); //TODO: use specific exception + throw Exception("Invalid frame prefix: $framePrefix"); // TODO: specific exception } if (data.lengthInBytes < 5) { - throw Exception("Data too short to parse"); //TODO: use specific exception + throw Exception("Data too short to parse"); // TODO: specific exception } - int sequenceNum = data.getUint8(1); - int cmd = data.getUint8(2); - int subOpcode = data.getUint8(3); - int status = data.getUint8(4); - ByteData payload = ByteData.sublistView(data, 5); + final int sequenceNum = data.getUint8(1); + final int cmd = data.getUint8(2); + final int subOpcode = data.getUint8(3); + final int status = data.getUint8(4); + final ByteData payload = ByteData.sublistView(data, 5); - Map dataHeader = { - "timestamp": baseTs, + logger.t("last sequenceNum: $_lastSeq, current sequenceNum: $sequenceNum"); + if (sequenceNum != _lastSeq) { + _lastSeq = sequenceNum; + _lastTs = 0; + logger.d("Sequence number changed. Resetting last timestamp."); + } + + // These header fields should go into every sample map + final Map baseHeader = { "sequenceNum": sequenceNum, "cmd": cmd, "subOpcode": subOpcode, "status": status, }; - - final List> parsedData; - + + List> result; switch (cmd) { case 0x40: // IMU switch (subOpcode) { - case 0x01: // Accel only - parsedData = _parseAccel(payload); - break; - case 0x06: // Accel + Gyro - parsedData = _parseAccelGyro(payload); - break; + case 0x01: // Accel only (6 bytes per sample) + result = _parseAccel( + data: payload, + receiveTs: _lastTs, + baseHeader: baseHeader, + ); + case 0x06: // Accel + Gyro (12 bytes per sample) + result = _parseAccelGyro( + data: payload, + receiveTs: _lastTs, + baseHeader: baseHeader, + ); default: throw Exception("Unknown sub-opcode for sensor data: $subOpcode"); } + default: throw Exception("Unknown command: $cmd"); } - - return parsedData.map((m) => m..addAll(dataHeader)).toList().first; //TODO: return full list + if (result.isNotEmpty) { + _lastTs = result.last["timestamp"] as int; + logger.t("Updated last timestamp to $_lastTs"); + } + return result; } - List> _parseAccel(ByteData data) { + List> _parseAccel({ + required ByteData data, + required int receiveTs, + required Map baseHeader, + }) { if (data.lengthInBytes % 6 != 0) { throw Exception("Invalid data length for Accel: ${data.lengthInBytes}"); } - List> parsedData = []; + + final int nSamples = data.lengthInBytes ~/ 6; + if (nSamples == 0) return const []; + + final List> parsedData = []; for (int i = 0; i < data.lengthInBytes; i += 6) { - if (i + 6 > data.lengthInBytes) break; - ByteData sample = ByteData.sublistView(data, i, i + 6); - Map accelData = _parseImuComp(sample); - parsedData.add({'Accelerometer': accelData}); + final int sampleIndex = i ~/ 6; + final int ts = receiveTs + sampleIndex * _samplePeriodMs; + + final ByteData sample = ByteData.sublistView(data, i, i + 6); + final Map accelData = _parseImuComp(sample); + + parsedData.add({ + ...baseHeader, + "timestamp": ts, + "Accelerometer": accelData, + }); } return parsedData; } - List> _parseAccelGyro(ByteData data) { + List> _parseAccelGyro({ + required ByteData data, + required int receiveTs, + required Map baseHeader, + }) { if (data.lengthInBytes % 12 != 0) { throw Exception("Invalid data length for Accel+Gyro: ${data.lengthInBytes}"); } - List> parsedData = []; + + final int nSamples = data.lengthInBytes ~/ 12; + if (nSamples == 0) return const []; + + final List> parsedData = []; for (int i = 0; i < data.lengthInBytes; i += 12) { - if (i + 12 > data.lengthInBytes) break; - ByteData sample = ByteData.sublistView(data, i, i + 12); - Map accelData = _parseImuComp(ByteData.sublistView(sample, 0, 6)); - Map gyroData = _parseImuComp(ByteData.sublistView(sample, 6)); + final int sampleIndex = i ~/ 12; + final int ts = receiveTs + sampleIndex * _samplePeriodMs; + + final ByteData sample = ByteData.sublistView(data, i, i + 12); + final ByteData accBytes = ByteData.sublistView(sample, 0, 6); + final ByteData gyroBytes = ByteData.sublistView(sample, 6); + + final Map accelData = _parseImuComp(accBytes); + final Map gyroData = _parseImuComp(gyroBytes); + parsedData.add({ - 'Accelerometer': accelData, - 'Gyroscope': gyroData, + ...baseHeader, + "timestamp": ts, + "Accelerometer": accelData, + "Gyroscope": gyroData, }); } return parsedData; } Map _parseImuComp(ByteData data) { - Map parsedComp = {}; - - parsedComp['X'] = data.getInt16(0, Endian.little); - parsedComp['Y'] = data.getInt16(2, Endian.little); - parsedComp['Z'] = data.getInt16(4, Endian.little); - - return parsedComp; + return { + 'X': data.getInt16(0, Endian.little), + 'Y': data.getInt16(2, Endian.little), + 'Z': data.getInt16(4, Endian.little), + }; } } From 47b8d8b55827e13205ed6a4314864091ddd971a0 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:15:48 +0100 Subject: [PATCH 12/12] lib/src/managers/tau_sensor_handler.dart: handle buffered data --- example/pubspec.lock | 2 +- lib/src/managers/tau_sensor_handler.dart | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/example/pubspec.lock b/example/pubspec.lock index fc44f0e..d3af3b6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -342,7 +342,7 @@ packages: path: ".." relative: true source: path - version: "2.2.2" + version: "2.2.3" path: dependency: transitive description: diff --git a/lib/src/managers/tau_sensor_handler.dart b/lib/src/managers/tau_sensor_handler.dart index f13d504..93d0c01 100644 --- a/lib/src/managers/tau_sensor_handler.dart +++ b/lib/src/managers/tau_sensor_handler.dart @@ -36,9 +36,9 @@ class TauSensorHandler extends SensorHandler { characteristicId: TauRingGatt.rxChar, ).listen( (data) async { - if (data.isNotEmpty && data[2] == sensorId) { - Map parsedData = await _parseData(data); - streamController.add(parsedData); + List> parsedData = await _parseData(data); + for (var d in parsedData) { + streamController.add(d); } }, onError: (error) { @@ -66,7 +66,7 @@ class TauSensorHandler extends SensorHandler { } /// Parses raw sensor data bytes into a [Map] of sensor values. - Future> _parseData(data) async { + Future>> _parseData(List data) async { ByteData byteData = ByteData.sublistView(Uint8List.fromList(data)); return _sensorValueParser.parse(byteData, []);