From 8399b79964acfd4ca48d13154a41324308ac3d6a Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 30 Nov 2025 01:04:36 +0100 Subject: [PATCH 1/8] rough esense_implementation, sensor parsing not working yet --- lib/open_earable_flutter.dart | 2 + lib/src/managers/esense_sensor_handler.dart | 200 ++++++++++++++++++ .../esense/esense_sensor_configuration.dart | 91 ++++++++ .../esense/sensor_range_option.dart | 25 +++ lib/src/models/devices/esense.dart | 58 +++++ lib/src/models/devices/esense_factory.dart | 136 ++++++++++++ .../esense_sensor_value_parser.dart | 105 +++++++++ 7 files changed, 617 insertions(+) create mode 100644 lib/src/managers/esense_sensor_handler.dart create mode 100644 lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart create mode 100644 lib/src/models/capabilities/sensor_configuration_specializations/esense/sensor_range_option.dart create mode 100644 lib/src/models/devices/esense.dart create mode 100644 lib/src/models/devices/esense_factory.dart create mode 100644 lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart diff --git a/lib/open_earable_flutter.dart b/lib/open_earable_flutter.dart index d009331..d6b3387 100644 --- a/lib/open_earable_flutter.dart +++ b/lib/open_earable_flutter.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:logger/logger.dart'; import 'package:meta/meta.dart'; import 'package:open_earable_flutter/src/models/devices/cosinuss_one_factory.dart'; +import 'package:open_earable_flutter/src/models/devices/esense_factory.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_factory.dart'; import 'package:open_earable_flutter/src/models/devices/open_earable_v2.dart'; import 'package:open_earable_flutter/src/models/devices/polar_factory.dart'; @@ -108,6 +109,7 @@ class WearableManager { PolarFactory(), DevKitFactory(), TauRingFactory(), + EsenseFactory(), ]; factory WearableManager() { diff --git a/lib/src/managers/esense_sensor_handler.dart b/lib/src/managers/esense_sensor_handler.dart new file mode 100644 index 0000000..62ca4bc --- /dev/null +++ b/lib/src/managers/esense_sensor_handler.dart @@ -0,0 +1,200 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart'; +import 'package:open_earable_flutter/src/utils/sensor_value_parser/sensor_value_parser.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../../open_earable_flutter.dart' show logger; +import '../models/devices/discovered_device.dart'; +import '../models/devices/esense.dart'; +import 'ble_gatt_manager.dart'; +import 'sensor_handler.dart'; + +class EsenseSensorHandler extends SensorHandler { + final BleGattManager _bleGattManager; + final DiscoveredDevice _discoveredDevice; + + final SensorValueParser _sensorValueParser; + + final Map _sensorConfigIdMap = { + 0x53: 0x55, // 9-axis IMU + }; + + EsenseSensorHandler({ + required BleGattManager bleGattManager, + required DiscoveredDevice discoveredDevice, + required SensorValueParser sensorValueParser, + }) : _bleGattManager = bleGattManager, + _discoveredDevice = discoveredDevice, + _sensorValueParser = sensorValueParser; + + @override + Stream> subscribeToSensorData(int sensorId) { + if (!_bleGattManager.isConnected(_discoveredDevice.id)) { + throw Exception("Can't subscribe to sensor data. Earable not connected"); + } + logger.t("Subscribing to Esense sensor data for sensor ID: 0x${sensorId.toRadixString(16).toUpperCase()} at characteristic $esenseSensorDataCharacteristicUuid"); + + StreamController> streamController = + StreamController(); + + _bleGattManager + .subscribe( + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseSensorDataCharacteristicUuid, + ) + .listen( + (data) async { + // logger.t("Received raw Esense: $data"); + + //TODO: check somehow if the sensor ID matches + if (data.isNotEmpty) { + List> parsedData = await _parseData(data); + + logger.t("Received parsed Esense data: $parsedData"); + + for (var d in parsedData) { + streamController.add(d); + } + } + }, + onError: (error) async { + logger.e("Error while subscribing to sensor data: $error"); + }, + ); + + return streamController.stream; + } + + @override + Future writeSensorConfig(EsenseSensorConfig sensorConfig) async { + if (!_bleGattManager.isConnected(_discoveredDevice.id)) { + throw Exception("Can't write sensor config. Earable not connected"); + } + + int on = sensorConfig.streamData ? 0x1 : 0x0; + int sampleRate = sensorConfig.sampleRate; + + List command = + _buildCommand(header: sensorConfig.sensorId, data: [on, sampleRate]); + + logger.t( + "Writing Esense sensor config: [${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", + ); + await _bleGattManager.write( + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseSensorConfigCharacteristicUuid, + byteData: command, + ); + + // logger.t("Reading back Esense sensor config to verify write..."); + + // List response = await _bleGattManager.read( + // deviceId: _discoveredDevice.id, + // serviceId: esenseServiceUuid, + // characteristicId: esenseSensorConfigCharacteristicUuid, + // ); + + // if (!listEquals(command, response)) { + // throw Exception( + // "Failed to write sensor config. Response does not match command." + // " Sent: [${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}], " + // "Received: [${response.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", + // ); + // } + } + + Uint8List _buildCommand({required int header, required List data}) { + int dataSize = data.length; + int checkSum = (dataSize + data.reduce((a, b) => a + b)) & 0xFF; + return Uint8List.fromList([ + header, + checkSum, + dataSize, + ...data, + ]); + } + + EsenseSensorConfig _buildSensorConfig(List data) { + if (data.length != 5) { + throw Exception("Invalid sensor config data length: ${data.length}"); + } + int sensorId = data[0]; + int dataSize = data[2]; + if (dataSize != 2) { + throw Exception("Invalid sensor config data size: $dataSize. Expected 2. Full data: ${data.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}"); + } + bool streamData = data[3] == 0x1; + int sampleRate = data[4]; + + return EsenseSensorConfig( + sensorId: sensorId, + sampleRate: sampleRate, + streamData: streamData, + ); + } + + Future>> _parseData(List data) async { + List commandData = await _bleGattManager.read( + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseSensorConfigCharacteristicUuid, + ); + EsenseSensorConfig sensorConfig = _buildSensorConfig( + commandData, + ); + + logger.t("Esense sensor config for parsing: $sensorConfig"); + + if (!_sensorConfigIdMap.containsKey(sensorConfig.sensorId)) { + throw Exception("Unknown sensor ID in config: 0x${sensorConfig.sensorId.toRadixString(16).toUpperCase()}"); + } + + SensorScheme scheme = SensorScheme( + _sensorConfigIdMap[sensorConfig.sensorId]!, + "6-axis IMU", + 0, + SensorConfigOptions( + [SensorConfigFeatures.frequencyDefinition], + SensorConfigFrequencies(0, 0, [sensorConfig.sampleRate.toDouble()]), + ), + ); + + List> parsedData = _sensorValueParser.parse( + ByteData.sublistView(Uint8List.fromList(data)), + [scheme], + ); + + logger.t("Parsed Esense sensor data: $parsedData"); + //TODO: Implement Esense data parsing logic + throw UnimplementedError(); + } +} + +class EsenseSensorConfig extends SensorConfig { + int sensorId; + int sampleRate; + bool streamData; + + EsenseSensorConfig({ + required this.sensorId, + required this.sampleRate, + required this.streamData, + }); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EsenseSensorConfig && + other.sensorId == sensorId && + other.sampleRate == sampleRate && + other.streamData == streamData; + } + + @override + int get hashCode => sensorId.hashCode ^ sampleRate.hashCode ^ streamData.hashCode; +} diff --git a/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart b/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart new file mode 100644 index 0000000..b357e5e --- /dev/null +++ b/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart @@ -0,0 +1,91 @@ +import 'package:flutter/foundation.dart'; +import 'package:open_earable_flutter/src/models/devices/esense.dart'; + +import '../../../../managers/esense_sensor_handler.dart'; +import '../../../../managers/sensor_handler.dart'; +import '../configurable_sensor_configuration.dart'; +import '../sensor_frequency_configuration.dart'; +import '../streamable_sensor_configuration.dart'; + +class EsenseSensorConfiguration extends SensorFrequencyConfiguration + implements ConfigurableSensorConfiguration { + final int _sensorCommand; + final Set _availableOptions; + + final SensorHandler _sensorHandler; + + @override + Set get availableOptions => _availableOptions; + + EsenseSensorConfiguration({ + required super.name, + required super.values, + required int sensorCommand, + required SensorHandler sensorHandler, + Set availableOptions = const {}, + super.offValue, + }) : _sensorCommand = sensorCommand, + _sensorHandler = sensorHandler, + _availableOptions = availableOptions; + + @override + void setConfiguration(EsenseSensorConfigurationValue configuration) { + EsenseSensorConfig sensorConfig = EsenseSensorConfig( + sensorId: _sensorCommand, + sampleRate: configuration.frequencyHz.round(), + streamData: configuration.options.any((option) => option is StreamSensorConfigOption), + ); + _sensorHandler.writeSensorConfig(sensorConfig); + } +} + +// MARK: Value + +class EsenseSensorConfigurationValue extends SensorFrequencyConfigurationValue + implements ConfigurableSensorConfigurationValue { + @override + final Set options; + + EsenseSensorConfigurationValue({ + required super.frequencyHz, + this.options = const {}, + }) : super(key: '${frequencyHz}Hz ${_optionsToString(options)}'); + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is EsenseSensorConfigurationValue && + other.frequencyHz == frequencyHz && + other.options.length == options.length && + setEquals(other.options, options); + } + @override + int get hashCode => frequencyHz.hashCode ^ options.hashCode; + + static String _optionsToString(Set options) { + String trailer = "off"; + if (options.any((option) => option is StreamSensorConfigOption)) { + trailer = "stream"; + } + return trailer; + } + + @override + ConfigurableSensorConfigurationValue withoutOptions() { + return EsenseSensorConfigurationValue( + frequencyHz: frequencyHz, + options: {}, + ); + } + + EsenseSensorConfigurationValue copyWith({ + double? frequencyHz, + Set? options, + }) { + return EsenseSensorConfigurationValue( + frequencyHz: frequencyHz ?? this.frequencyHz, + options: options ?? this.options, + ); + } +} diff --git a/lib/src/models/capabilities/sensor_configuration_specializations/esense/sensor_range_option.dart b/lib/src/models/capabilities/sensor_configuration_specializations/esense/sensor_range_option.dart new file mode 100644 index 0000000..c9a00d8 --- /dev/null +++ b/lib/src/models/capabilities/sensor_configuration_specializations/esense/sensor_range_option.dart @@ -0,0 +1,25 @@ +import '../configurable_sensor_configuration.dart'; + +abstract interface class Range { + const Range(); +} + +enum GyroRange implements Range { + range250DPS, + range500DPS, + range1000DPS, + range2000DPS, +} + +enum AccelRange implements Range { + range2G, + range4G, + range8G, + range16G, +} + +class SensorRangeOption extends SensorConfigurationOption { + final R range; + + const SensorRangeOption({required super.name, required this.range}); +} diff --git a/lib/src/models/devices/esense.dart b/lib/src/models/devices/esense.dart new file mode 100644 index 0000000..4549b37 --- /dev/null +++ b/lib/src/models/devices/esense.dart @@ -0,0 +1,58 @@ +import 'package:open_earable_flutter/src/models/capabilities/sensor.dart'; + +import 'package:open_earable_flutter/src/models/capabilities/sensor_configuration.dart'; + +import '../../managers/ble_gatt_manager.dart'; +import '../capabilities/sensor_configuration_manager.dart'; +import '../capabilities/sensor_manager.dart'; +import 'discovered_device.dart'; +import 'wearable.dart'; + +const String esenseServiceUuid = "ff06"; +const String esenseSensorConfigCharacteristicUuid = "ff07"; +const String esenseSensorDataCharacteristicUuid = "0000ff08-0000-1000-8000-00805f9b34fb"; + +class Esense extends Wearable + implements SensorManager, SensorConfigurationManager { + final DiscoveredDevice _discoveredDevice; + final BleGattManager _bleManager; + + final List> _sensorConfigs; + final List> _sensors; + + @override + String get deviceId => _discoveredDevice.id; + + Esense({ + required super.name, + required super.disconnectNotifier, + required BleGattManager bleManager, + required DiscoveredDevice discoveredDevice, + List> sensorConfigurations = const [], + List> sensors = const [], + }) : _discoveredDevice = discoveredDevice, + _bleManager = bleManager, + _sensorConfigs = sensorConfigurations, + _sensors = sensors; + + @override + Future disconnect() { + return _bleManager.disconnect(deviceId); + } + + @override + // TODO: implement sensorConfigurationStream + Stream< + Map, + SensorConfigurationValue>> get sensorConfigurationStream => + Stream.empty(); + + @override + // TODO: implement sensorConfigurations + List> + get sensorConfigurations => _sensorConfigs; + + @override + // TODO: implement sensors + List> get sensors => _sensors; +} diff --git a/lib/src/models/devices/esense_factory.dart b/lib/src/models/devices/esense_factory.dart new file mode 100644 index 0000000..e07c079 --- /dev/null +++ b/lib/src/models/devices/esense_factory.dart @@ -0,0 +1,136 @@ +import 'dart:async'; + +import 'package:universal_ble/universal_ble.dart'; + +import '../../managers/esense_sensor_handler.dart'; +import '../../managers/sensor_handler.dart'; +import '../../utils/sensor_value_parser/esense_sensor_value_parser.dart'; +import '../capabilities/sensor.dart'; +import '../capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart'; +import '../capabilities/sensor_configuration_specializations/streamable_sensor_configuration.dart'; +import '../wearable_factory.dart'; +import 'discovered_device.dart'; +import 'esense.dart'; +import 'wearable.dart'; + +class EsenseFactory extends WearableFactory { + @override + Future createFromDevice(DiscoveredDevice device, + {Set options = const {},}) async { + + EsenseSensorHandler sensorHandler = EsenseSensorHandler( + bleGattManager: bleManager!, + discoveredDevice: device, + sensorValueParser: EsenseSensorValueParser(), + ); + + List imuConfigValues = [ + EsenseSensorConfigurationValue(frequencyHz: 0.0), + EsenseSensorConfigurationValue(frequencyHz: 25.0), + EsenseSensorConfigurationValue(frequencyHz: 50.0), + EsenseSensorConfigurationValue(frequencyHz: 100.0), + EsenseSensorConfigurationValue(frequencyHz: 200.0), + ].expand((v) => [v, v.copyWith(options: {StreamSensorConfigOption()})]).toList(); + + Esense esense = Esense( + name: device.name, + bleManager: bleManager!, + discoveredDevice: device, + disconnectNotifier: disconnectNotifier!, + sensorConfigurations: [ + EsenseSensorConfiguration( + name: "9-axis IMU", + values: imuConfigValues, + sensorCommand: 0x53, + sensorHandler: sensorHandler, + availableOptions: { + StreamSensorConfigOption(), + }, + ), + ], + sensors: [ + EsenseSensor( + sensorId: 0x55, + sensorName: "Accelerometer", + chartTitle: "Accelerometer", + shortChartTitle: "Accel", + axisNames: ["X", "Y", "Z"], + axisUnits: ["g", "g", "g"], + sensorHandler: sensorHandler, + ), + EsenseSensor( + sensorId: 0x55, + sensorName: "Gyroscope", + chartTitle: "Gyroscope", + shortChartTitle: "Gyro", + axisNames: ["X", "Y", "Z"], + axisUnits: ["dps", "dps", "dps"], + sensorHandler: sensorHandler, + ), + ], + ); + + return esense; + } + + @override + Future matches(DiscoveredDevice device, List services) async { + return RegExp(r'^eSense-\d{4}$').hasMatch(device.name); + } +} + +class EsenseSensor extends Sensor { + final List _axisNames; + final List _axisUnits; + + final int _sensorId; + + final SensorHandler _sensorHandler; + + EsenseSensor({ + required int sensorId, + required super.sensorName, + required super.chartTitle, + required super.shortChartTitle, + required List axisNames, + required List axisUnits, + required SensorHandler sensorHandler, + }) : _axisNames = axisNames, + _axisUnits = axisUnits, + _sensorId = sensorId, + _sensorHandler = sensorHandler; + + @override + List get axisNames => _axisNames; + + @override + List get axisUnits => _axisUnits; + @override + Stream get sensorStream { + StreamController streamController = + StreamController(); + _sensorHandler.subscribeToSensorData(_sensorId).listen( + (data) { + int timestamp = data["timestamp"]; + + List values = []; + for (var entry in (data[sensorName] as Map).entries) { + if (entry.key == 'units') { + continue; + } + + values.add(entry.value); + } + + SensorDoubleValue sensorValue = SensorDoubleValue( + values: values, + timestamp: timestamp, + ); + + streamController.add(sensorValue); + }, + ); + + return streamController.stream; + } +} diff --git a/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart new file mode 100644 index 0000000..311e5dc --- /dev/null +++ b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart @@ -0,0 +1,105 @@ +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 EsenseSensorValueParser extends SensorValueParser { + // Maps to keep track of previous timestamps for sensors + // key: (sensorId, packetIndex), value: lastTimestamp + final Map<(int, int), int> _timestampMap = {}; + + @override + List> parse( + ByteData data, + List sensorSchemes, + ) { + int cmdHead = data.getUint8(0); + int packetIndex = data.getUint8(1); + int checkSum = data.getUint8(2); + int dataSize = data.getUint8(3); + + Uint8List payload = data.buffer.asUint8List(4); + + logger.t( + "Esense Sensor Data Received: cmdHead: $cmdHead, packetIndex: $packetIndex, checkSum: $checkSum, dataSize: $dataSize, payload: $payload", + ); + + if (payload.length != dataSize) { + throw Exception( + "Data size mismatch. Expected $dataSize, got ${payload.length}", + ); + } + + final ByteData payloadData = + payload.buffer.asByteData(payload.offsetInBytes, dataSize); + if (!_verifyChecksum(payloadData, checkSum)) { + throw Exception("Checksum verification failed."); + } + + switch (cmdHead) { + case 0x55: + if (dataSize != 12) { + throw Exception( + "Invalid data size for sensor data packet. Expected 12, got $dataSize", + ); + } + + SensorScheme scheme = sensorSchemes.firstWhere( + (s) => s.sensorId == cmdHead, + orElse: () => throw Exception("Unknown sensorId: ${cmdHead.toRadixString(16)}, only got ${sensorSchemes.map((s) => s.sensorId.toRadixString(16)).toList()}"), + ); + SensorConfigFrequencies? frequencies = scheme.options?.frequencies; + + if (frequencies == null) { + throw Exception( + "Frequencies not defined for sensorId: $cmdHead", + ); + } + + double freq = frequencies.frequencies[frequencies.defaultFreqIndex]; + int tsIncrement = (1000 / freq).round(); + int lastTs = _timestampMap.putIfAbsent( + (cmdHead, packetIndex), + () => 0, + ); + int ts = lastTs + tsIncrement; + _timestampMap[(cmdHead, packetIndex)] = ts; + + int rawGyroX = payloadData.getInt16(0, Endian.little); + int rawGyroY = payloadData.getInt16(2, Endian.little); + int rawGyroZ = payloadData.getInt16(4, Endian.little); + int rawAccelX = payloadData.getInt16(6, Endian.little); + int rawAccelY = payloadData.getInt16(8, Endian.little); + int rawAccelZ = payloadData.getInt16(10, Endian.little); + + Map output = { + "timestmap": ts, + "Accelerometer": { + "x": rawAccelX, + "y": rawAccelY, + "z": rawAccelZ, + }, + "Gyroscope": { + "x": rawGyroX, + "y": rawGyroY, + "z": rawGyroZ, + }, + }; + + return [output]; + + default: + throw Exception("Unknown sensor ID: ${cmdHead.toRadixString(16)}"); + } + } + + bool _verifyChecksum(ByteData data, int expectedChecksum) { + int calculatedChecksum = data.lengthInBytes; + for (int i = 0; i < data.lengthInBytes; i++) { + calculatedChecksum = (calculatedChecksum + data.getUint8(i)); + } + calculatedChecksum = calculatedChecksum & 0xFF; + return calculatedChecksum == expectedChecksum; + } +} From 2e2e5f879a974150f3bd9ca5d39535d49b01c956 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:37:31 +0100 Subject: [PATCH 2/8] lib/src/managers/esense_sensor_handler.dart: cache last sensor config to reduce overhead --- lib/src/managers/esense_sensor_handler.dart | 189 ++++++++++++++------ 1 file changed, 130 insertions(+), 59 deletions(-) diff --git a/lib/src/managers/esense_sensor_handler.dart b/lib/src/managers/esense_sensor_handler.dart index 62ca4bc..8330c19 100644 --- a/lib/src/managers/esense_sensor_handler.dart +++ b/lib/src/managers/esense_sensor_handler.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart'; import 'package:open_earable_flutter/src/utils/sensor_value_parser/sensor_value_parser.dart'; -import 'package:universal_ble/universal_ble.dart'; import '../../open_earable_flutter.dart' show logger; import '../models/devices/discovered_device.dart'; @@ -14,13 +13,21 @@ import 'sensor_handler.dart'; class EsenseSensorHandler extends SensorHandler { final BleGattManager _bleGattManager; final DiscoveredDevice _discoveredDevice; - final SensorValueParser _sensorValueParser; - final Map _sensorConfigIdMap = { + /// Maps eSense sensor config ID -> data packet command header. + /// For now: + /// 0x53 (IMU config cmd) -> 0x55 (IMU data packet header) + final Map _sensorConfigIdMap = const { 0x53: 0x55, // 9-axis IMU }; + /// Last known sensor configuration (either written by us or read once). + EsenseSensorConfig? _cachedSensorConfig; + + /// Cached SensorScheme built from [_cachedSensorConfig]. + SensorScheme? _cachedSensorScheme; + EsenseSensorHandler({ required BleGattManager bleGattManager, required DiscoveredDevice discoveredDevice, @@ -34,37 +41,50 @@ class EsenseSensorHandler extends SensorHandler { if (!_bleGattManager.isConnected(_discoveredDevice.id)) { throw Exception("Can't subscribe to sensor data. Earable not connected"); } - logger.t("Subscribing to Esense sensor data for sensor ID: 0x${sensorId.toRadixString(16).toUpperCase()} at characteristic $esenseSensorDataCharacteristicUuid"); - StreamController> streamController = - StreamController(); + logger.t( + "Subscribing to Esense sensor data for sensor ID: 0x${sensorId.toRadixString(16).toUpperCase()} " + "at characteristic $esenseSensorDataCharacteristicUuid", + ); + + final streamController = StreamController>(); - _bleGattManager + final subscription = _bleGattManager .subscribe( - deviceId: _discoveredDevice.id, - serviceId: esenseServiceUuid, - characteristicId: esenseSensorDataCharacteristicUuid, - ) + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseSensorDataCharacteristicUuid, + ) .listen( (data) async { - // logger.t("Received raw Esense: $data"); + if (data.isEmpty) return; + + final parsedData = await _parseData(data); - //TODO: check somehow if the sensor ID matches - if (data.isNotEmpty) { - List> parsedData = await _parseData(data); + logger.t("Received parsed Esense data: $parsedData"); - logger.t("Received parsed Esense data: $parsedData"); - - for (var d in parsedData) { + for (final d in parsedData) { + if (!streamController.isClosed) { streamController.add(d); } } }, - onError: (error) async { + onError: (error) { logger.e("Error while subscribing to sensor data: $error"); + if (!streamController.isClosed) { + streamController.addError(error); + } + }, + onDone: () { + if (!streamController.isClosed) { + streamController.close(); + } }, ); + // Ensure BLE subscription is cancelled when the consumer cancels our stream. + streamController.onCancel = () => subscription.cancel(); + return streamController.stream; } @@ -74,15 +94,17 @@ class EsenseSensorHandler extends SensorHandler { throw Exception("Can't write sensor config. Earable not connected"); } - int on = sensorConfig.streamData ? 0x1 : 0x0; - int sampleRate = sensorConfig.sampleRate; + final on = sensorConfig.streamData ? 0x1 : 0x0; + final sampleRate = sensorConfig.sampleRate; - List command = + final command = _buildCommand(header: sensorConfig.sensorId, data: [on, sampleRate]); logger.t( - "Writing Esense sensor config: [${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", + "Writing Esense sensor config: " + "[${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", ); + await _bleGattManager.write( deviceId: _discoveredDevice.id, serviceId: esenseServiceUuid, @@ -90,26 +112,32 @@ class EsenseSensorHandler extends SensorHandler { byteData: command, ); - // logger.t("Reading back Esense sensor config to verify write..."); - - // List response = await _bleGattManager.read( - // deviceId: _discoveredDevice.id, - // serviceId: esenseServiceUuid, - // characteristicId: esenseSensorConfigCharacteristicUuid, - // ); - - // if (!listEquals(command, response)) { - // throw Exception( - // "Failed to write sensor config. Response does not match command." - // " Sent: [${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}], " - // "Received: [${response.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", - // ); - // } + List receivedCommand = await _bleGattManager.read( + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseSensorConfigCharacteristicUuid, + ); + + if (!listEquals(receivedCommand, command)) { + throw Exception( + "Esense sensor config write verification failed. " + "Wrote: [${command.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}], " + "Read back: [${receivedCommand.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}]", + ); + } + + // Update local cache: we assume our write is authoritative. + _cachedSensorConfig = sensorConfig; + _cachedSensorScheme = null; // force rebuild with new sample rate/header } + // MARK: - Helpers + Uint8List _buildCommand({required int header, required List data}) { - int dataSize = data.length; - int checkSum = (dataSize + data.reduce((a, b) => a + b)) & 0xFF; + final dataSize = data.length; + final sum = data.fold(dataSize, (acc, b) => acc + b); + final checkSum = sum & 0xFF; + return Uint8List.fromList([ header, checkSum, @@ -122,13 +150,18 @@ class EsenseSensorHandler extends SensorHandler { if (data.length != 5) { throw Exception("Invalid sensor config data length: ${data.length}"); } - int sensorId = data[0]; - int dataSize = data[2]; + + final sensorId = data[0]; + final dataSize = data[2]; if (dataSize != 2) { - throw Exception("Invalid sensor config data size: $dataSize. Expected 2. Full data: ${data.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}"); + throw Exception( + "Invalid sensor config data size: $dataSize. Expected 2. " + "Full data: ${data.map((b) => '0x${b.toRadixString(16).padLeft(2, '0').toUpperCase()}').join(', ')}", + ); } - bool streamData = data[3] == 0x1; - int sampleRate = data[4]; + + final streamData = data[3] == 0x1; + final sampleRate = data[4]; return EsenseSensorConfig( sensorId: sensorId, @@ -137,40 +170,78 @@ class EsenseSensorHandler extends SensorHandler { ); } - Future>> _parseData(List data) async { - List commandData = await _bleGattManager.read( + /// Returns the current sensor config. + /// Prefers local cache (what we last wrote). If none, reads once from the device. + Future _getSensorConfig() async { + // 1. Use cached config if available (we wrote it ourselves). + final cached = _cachedSensorConfig; + if (cached != null) { + return cached; + } + + // 2. Otherwise read once from the device and cache it. + final commandData = await _bleGattManager.read( deviceId: _discoveredDevice.id, serviceId: esenseServiceUuid, characteristicId: esenseSensorConfigCharacteristicUuid, ); - EsenseSensorConfig sensorConfig = _buildSensorConfig( - commandData, - ); - logger.t("Esense sensor config for parsing: $sensorConfig"); + final config = _buildSensorConfig(commandData); + + logger.t("Esense sensor config read from device: $config"); - if (!_sensorConfigIdMap.containsKey(sensorConfig.sensorId)) { - throw Exception("Unknown sensor ID in config: 0x${sensorConfig.sensorId.toRadixString(16).toUpperCase()}"); + _cachedSensorConfig = config; + _cachedSensorScheme = null; // rebuild scheme based on this config + + return config; + } + + /// Returns a SensorScheme built from the current config, caching the result. + Future _getSensorScheme() async { + final cachedScheme = _cachedSensorScheme; + if (cachedScheme != null) { + return cachedScheme; } - SensorScheme scheme = SensorScheme( - _sensorConfigIdMap[sensorConfig.sensorId]!, + final sensorConfig = await _getSensorConfig(); + + final header = _sensorConfigIdMap[sensorConfig.sensorId]; + if (header == null) { + throw Exception( + "Unknown sensor ID in config: 0x${sensorConfig.sensorId.toRadixString(16).toUpperCase()}", + ); + } + + final scheme = SensorScheme( + header, "6-axis IMU", 0, SensorConfigOptions( [SensorConfigFeatures.frequencyDefinition], - SensorConfigFrequencies(0, 0, [sensorConfig.sampleRate.toDouble()]), + SensorConfigFrequencies( + 0, + 0, + [sensorConfig.sampleRate.toDouble()], + ), ), ); - List> parsedData = _sensorValueParser.parse( + _cachedSensorScheme = scheme; + return scheme; + } + + Future>> _parseData(List data) async { + final scheme = await _getSensorScheme(); + + final parsedData = _sensorValueParser.parse( ByteData.sublistView(Uint8List.fromList(data)), [scheme], ); logger.t("Parsed Esense sensor data: $parsedData"); - //TODO: Implement Esense data parsing logic - throw UnimplementedError(); + // TODO: scale raw values according to sensor specifications + + return parsedData; } } From b5c75a635cad9f9aa6617a4db0b91b77fbf9499d Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:38:22 +0100 Subject: [PATCH 3/8] lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart: fixed timestamp resetting too often --- .../esense_sensor_value_parser.dart | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart index 311e5dc..6106ffa 100644 --- a/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart +++ b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart @@ -6,8 +6,8 @@ import 'sensor_value_parser.dart'; class EsenseSensorValueParser extends SensorValueParser { // Maps to keep track of previous timestamps for sensors - // key: (sensorId, packetIndex), value: lastTimestamp - final Map<(int, int), int> _timestampMap = {}; + // key: sensorId, value: lastTimestamp + final Map _timestampMap = {}; @override List> parse( @@ -60,12 +60,11 @@ class EsenseSensorValueParser extends SensorValueParser { double freq = frequencies.frequencies[frequencies.defaultFreqIndex]; int tsIncrement = (1000 / freq).round(); int lastTs = _timestampMap.putIfAbsent( - (cmdHead, packetIndex), + cmdHead, () => 0, ); int ts = lastTs + tsIncrement; - _timestampMap[(cmdHead, packetIndex)] = ts; - + _timestampMap[cmdHead] = ts; int rawGyroX = payloadData.getInt16(0, Endian.little); int rawGyroY = payloadData.getInt16(2, Endian.little); int rawGyroZ = payloadData.getInt16(4, Endian.little); @@ -74,7 +73,7 @@ class EsenseSensorValueParser extends SensorValueParser { int rawAccelZ = payloadData.getInt16(10, Endian.little); Map output = { - "timestmap": ts, + "timestamp": ts, "Accelerometer": { "x": rawAccelX, "y": rawAccelY, From 483724349786c6558f2b3ac6e1defdf270a77dca Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Sun, 30 Nov 2025 15:40:28 +0100 Subject: [PATCH 4/8] lib/src/models/devices/esense_factory.dart: convert sensor value to double --- lib/src/models/devices/esense_factory.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/models/devices/esense_factory.dart b/lib/src/models/devices/esense_factory.dart index e07c079..096c7db 100644 --- a/lib/src/models/devices/esense_factory.dart +++ b/lib/src/models/devices/esense_factory.dart @@ -25,7 +25,6 @@ class EsenseFactory extends WearableFactory { ); List imuConfigValues = [ - EsenseSensorConfigurationValue(frequencyHz: 0.0), EsenseSensorConfigurationValue(frequencyHz: 25.0), EsenseSensorConfigurationValue(frequencyHz: 50.0), EsenseSensorConfigurationValue(frequencyHz: 100.0), @@ -119,7 +118,13 @@ class EsenseSensor extends Sensor { continue; } - values.add(entry.value); + if (entry.value is int) { + values.add((entry.value as int).toDouble()); + } else if (entry.value is double) { + values.add(entry.value as double); + } else { + throw Exception("Unsupported sensor value type: ${entry.value.runtimeType}"); + } } SensorDoubleValue sensorValue = SensorDoubleValue( From c5661fc34c0ccddd28d49439c5fa487b6d8b23c5 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 1 Dec 2025 12:17:53 +0100 Subject: [PATCH 5/8] lib/src/models/devices/esense.dart: added charac for imu configuration --- lib/src/models/devices/esense.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/models/devices/esense.dart b/lib/src/models/devices/esense.dart index 4549b37..ed5b318 100644 --- a/lib/src/models/devices/esense.dart +++ b/lib/src/models/devices/esense.dart @@ -11,6 +11,7 @@ import 'wearable.dart'; const String esenseServiceUuid = "ff06"; const String esenseSensorConfigCharacteristicUuid = "ff07"; const String esenseSensorDataCharacteristicUuid = "0000ff08-0000-1000-8000-00805f9b34fb"; +const String esenseImuConfigCharacteristicUuid = "ff0e"; class Esense extends Wearable implements SensorManager, SensorConfigurationManager { @@ -48,11 +49,9 @@ class Esense extends Wearable Stream.empty(); @override - // TODO: implement sensorConfigurations List> get sensorConfigurations => _sensorConfigs; @override - // TODO: implement sensors List> get sensors => _sensors; } From 959f0b32b2c84602166fb8e53e7eac764e274925 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:43:18 +0100 Subject: [PATCH 6/8] lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart: changed endianess to big endian --- .../esense_sensor_value_parser.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart index 6106ffa..412f697 100644 --- a/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart +++ b/lib/src/utils/sensor_value_parser/esense_sensor_value_parser.dart @@ -65,12 +65,12 @@ class EsenseSensorValueParser extends SensorValueParser { ); int ts = lastTs + tsIncrement; _timestampMap[cmdHead] = ts; - int rawGyroX = payloadData.getInt16(0, Endian.little); - int rawGyroY = payloadData.getInt16(2, Endian.little); - int rawGyroZ = payloadData.getInt16(4, Endian.little); - int rawAccelX = payloadData.getInt16(6, Endian.little); - int rawAccelY = payloadData.getInt16(8, Endian.little); - int rawAccelZ = payloadData.getInt16(10, Endian.little); + int rawGyroX = payloadData.getInt16(0, Endian.big); + int rawGyroY = payloadData.getInt16(2, Endian.big); + int rawGyroZ = payloadData.getInt16(4, Endian.big); + int rawAccelX = payloadData.getInt16(6, Endian.big); + int rawAccelY = payloadData.getInt16(8, Endian.big); + int rawAccelZ = payloadData.getInt16(10, Endian.big); Map output = { "timestamp": ts, From 38c9b7aabc493e97baf50d6222009e441b649a95 Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:45:45 +0100 Subject: [PATCH 7/8] lib/src/managers/esense_sensor_handler.dart: parse values according to their range --- lib/src/managers/esense_sensor_handler.dart | 155 ++++++++++++++++++-- 1 file changed, 142 insertions(+), 13 deletions(-) diff --git a/lib/src/managers/esense_sensor_handler.dart b/lib/src/managers/esense_sensor_handler.dart index 8330c19..f378ba0 100644 --- a/lib/src/managers/esense_sensor_handler.dart +++ b/lib/src/managers/esense_sensor_handler.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:open_earable_flutter/src/utils/sensor_scheme_parser/sensor_scheme_reader.dart'; -import 'package:open_earable_flutter/src/utils/sensor_value_parser/sensor_value_parser.dart'; import '../../open_earable_flutter.dart' show logger; +import '../models/capabilities/sensor_configuration_specializations/esense/sensor_range_option.dart'; import '../models/devices/discovered_device.dart'; import '../models/devices/esense.dart'; +import '../utils/sensor_scheme_parser/sensor_scheme_reader.dart'; +import '../utils/sensor_value_parser/sensor_value_parser.dart'; import 'ble_gatt_manager.dart'; import 'sensor_handler.dart'; @@ -22,19 +23,37 @@ class EsenseSensorHandler extends SensorHandler { 0x53: 0x55, // 9-axis IMU }; + final Map _accelScaleFactors = const { + AccelRange.range2G: 16384, + AccelRange.range4G: 8192, + AccelRange.range8G: 4096, + AccelRange.range16G: 2048, + }; + final Map _gyroScaleFactors = const { + GyroRange.range250DPS: 131, + GyroRange.range500DPS: 65.5, + GyroRange.range1000DPS: 32.8, + GyroRange.range2000DPS: 16.4, + }; + /// Last known sensor configuration (either written by us or read once). EsenseSensorConfig? _cachedSensorConfig; /// Cached SensorScheme built from [_cachedSensorConfig]. SensorScheme? _cachedSensorScheme; + AccelRange? _cachedAccelRange; + GyroRange? _cachedGyroRange; + EsenseSensorHandler({ required BleGattManager bleGattManager, required DiscoveredDevice discoveredDevice, required SensorValueParser sensorValueParser, }) : _bleGattManager = bleGattManager, _discoveredDevice = discoveredDevice, - _sensorValueParser = sensorValueParser; + _sensorValueParser = sensorValueParser { + _getImuRanges(); // prefetch ranges + } @override Stream> subscribeToSensorData(int sensorId) { @@ -83,7 +102,7 @@ class EsenseSensorHandler extends SensorHandler { ); // Ensure BLE subscription is cancelled when the consumer cancels our stream. - streamController.onCancel = () => subscription.cancel(); + streamController.onCancel = subscription.cancel; return streamController.stream; } @@ -112,7 +131,7 @@ class EsenseSensorHandler extends SensorHandler { byteData: command, ); - List receivedCommand = await _bleGattManager.read( + final List receivedCommand = await _bleGattManager.read( deviceId: _discoveredDevice.id, serviceId: esenseServiceUuid, characteristicId: esenseSensorConfigCharacteristicUuid, @@ -173,13 +192,11 @@ class EsenseSensorHandler extends SensorHandler { /// Returns the current sensor config. /// Prefers local cache (what we last wrote). If none, reads once from the device. Future _getSensorConfig() async { - // 1. Use cached config if available (we wrote it ourselves). final cached = _cachedSensorConfig; if (cached != null) { return cached; } - // 2. Otherwise read once from the device and cache it. final commandData = await _bleGattManager.read( deviceId: _discoveredDevice.id, serviceId: esenseServiceUuid, @@ -191,7 +208,7 @@ class EsenseSensorHandler extends SensorHandler { logger.t("Esense sensor config read from device: $config"); _cachedSensorConfig = config; - _cachedSensorScheme = null; // rebuild scheme based on this config + _cachedSensorScheme = null; return config; } @@ -230,18 +247,129 @@ class EsenseSensorHandler extends SensorHandler { return scheme; } + Future<(AccelRange, GyroRange)> _getImuRanges() async { + if (_cachedAccelRange != null && _cachedGyroRange != null) { + return (_cachedAccelRange!, _cachedGyroRange!); + } + + final raw = await _bleGattManager.read( + deviceId: _discoveredDevice.id, + serviceId: esenseServiceUuid, + characteristicId: esenseImuConfigCharacteristicUuid, + ); + + if (raw.length != 7) { + throw Exception( + "Invalid IMU config data length: ${raw.length}. Expected 7.", + ); + } + + if (raw[0] != 0x59) { + throw Exception( + "Invalid IMU config header: 0x${raw[0].toRadixString(16).toUpperCase()}. Expected 0x59.", + ); + } + + final int dataSize = raw[2]; + if (dataSize != 4) { + throw Exception( + "Invalid IMU config data size: $dataSize. Expected 4.", + ); + } + + final accelRangeByte = raw[5]; + final gyroRangeByte = raw[4]; + + switch ((accelRangeByte >> 3) & 0x03) { + case 0x00: + _cachedAccelRange = AccelRange.range2G; + case 0x01: + _cachedAccelRange = AccelRange.range4G; + case 0x02: + _cachedAccelRange = AccelRange.range8G; + case 0x03: + _cachedAccelRange = AccelRange.range16G; + default: + throw Exception( + "Unknown accelerometer range byte: 0x${accelRangeByte.toRadixString(16).toUpperCase()}", + ); + } + + switch ((gyroRangeByte >> 3) & 0x03) { + case 0x00: + _cachedGyroRange = GyroRange.range250DPS; + case 0x01: + _cachedGyroRange = GyroRange.range500DPS; + case 0x02: + _cachedGyroRange = GyroRange.range1000DPS; + case 0x03: + _cachedGyroRange = GyroRange.range2000DPS; + default: + throw Exception( + "Unknown gyroscope range byte: 0x${gyroRangeByte.toRadixString(16).toUpperCase()}", + ); + } + + logger.t("Loaded IMU ranges: Accel=$_cachedAccelRange, Gyro=$_cachedGyroRange"); + + return (_cachedAccelRange!, _cachedGyroRange!); + } + + /// Parse raw notification bytes, then convert accel to g and gyro to deg/s. Future>> _parseData(List data) async { final scheme = await _getSensorScheme(); + final (accelRange, gyroRange) = await _getImuRanges(); final parsedData = _sensorValueParser.parse( ByteData.sublistView(Uint8List.fromList(data)), [scheme], ); - logger.t("Parsed Esense sensor data: $parsedData"); - // TODO: scale raw values according to sensor specifications + final scaled = >[]; + for (final sample in parsedData) { + scaled.add(_applyImuScaling(sample, accelRange, gyroRange)); + } + + logger.t("Parsed & scaled Esense sensor data: $scaled"); + + return scaled; + } + + /// Applies scaling to convert ADC values to g / deg/s. + /// Adjust the keys ("acc_x", "gyro_x", ...) to match what your SensorValueParser produces. + Map _applyImuScaling( + Map sample, + AccelRange accelRange, + GyroRange gyroRange, + ) { + // Shallow copy of outer map + final result = Map.from(sample); + + // Make *new* mutable, dynamic-typed inner maps + final accel = Map.from(result['Accelerometer'] as Map); + final gyro = Map.from(result['Gyroscope'] as Map); + + // Accelerometer to g + for (final key in const ['x', 'y', 'z']) { + final raw = accel[key]; + if (raw is num) { + accel[key] = raw.toDouble() / _accelScaleFactors[accelRange]!; + } + } + + // Gyroscope to deg/s + for (final key in const ['x', 'y', 'z']) { + final raw = gyro[key]; + if (raw is num) { + gyro[key] = raw.toDouble() / _gyroScaleFactors[gyroRange]!; + } + } - return parsedData; + // Put updated inner maps back + result['Accelerometer'] = accel; + result['Gyroscope'] = gyro; + + return result; } } @@ -265,7 +393,8 @@ class EsenseSensorConfig extends SensorConfig { other.sampleRate == sampleRate && other.streamData == streamData; } - + @override - int get hashCode => sensorId.hashCode ^ sampleRate.hashCode ^ streamData.hashCode; + int get hashCode => + sensorId.hashCode ^ sampleRate.hashCode ^ streamData.hashCode; } From cd3d60a6d61b8a3f344fc4c24d6498870d07da0c Mon Sep 17 00:00:00 2001 From: Dennis Moschina <45356478+DennisMoschina@users.noreply.github.com> Date: Mon, 1 Dec 2025 14:05:41 +0100 Subject: [PATCH 8/8] lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart: removed unused import --- .../esense/esense_sensor_configuration.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart b/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart index b357e5e..ab9b973 100644 --- a/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart +++ b/lib/src/models/capabilities/sensor_configuration_specializations/esense/esense_sensor_configuration.dart @@ -1,5 +1,4 @@ import 'package:flutter/foundation.dart'; -import 'package:open_earable_flutter/src/models/devices/esense.dart'; import '../../../../managers/esense_sensor_handler.dart'; import '../../../../managers/sensor_handler.dart';