Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ packages:
path: ".."
relative: true
source: path
version: "2.2.2"
version: "2.2.3"
path:
dependency: transitive
description:
Expand Down
2 changes: 2 additions & 0 deletions lib/open_earable_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -106,6 +107,7 @@ class WearableManager {
CosinussOneFactory(),
PolarFactory(),
DevKitFactory(),
TauRingFactory(),
];

factory WearableManager() {
Expand Down
95 changes: 95 additions & 0 deletions lib/src/managers/tau_sensor_handler.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'dart:async';
import 'dart:typed_data';

import 'package:open_earable_flutter/src/models/devices/tau_ring.dart';

import '../../open_earable_flutter.dart';
import 'sensor_handler.dart';
import '../utils/sensor_value_parser/sensor_value_parser.dart';

class TauSensorHandler extends SensorHandler<TauSensorConfig> {
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<Map<String, dynamic>> subscribeToSensorData(int sensorId) {
if (!_bleManager.isConnected(_discoveredDevice.id)) {
throw Exception("Can't subscribe to sensor data. Earable not connected");
}

StreamController<Map<String, dynamic>> streamController =
StreamController();
_bleManager
.subscribe(
deviceId: _discoveredDevice.id,
serviceId: TauRingGatt.service,
characteristicId: TauRingGatt.rxChar,
).listen(
(data) async {
List<Map<String, dynamic>> parsedData = await _parseData(data);
for (var d in parsedData) {
streamController.add(d);
}
},
onError: (error) {
logger.e("Error while subscribing to sensor data: $error");
},
);

return streamController.stream;
}

@override
Future<void> writeSensorConfig(TauSensorConfig sensorConfig) async {
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.
Future<List<Map<String, dynamic>>> _parseData(List<int> data) async {
ByteData byteData = ByteData.sublistView(Uint8List.fromList(data));

return _sensorValueParser.parse(byteData, []);
}
}

class TauSensorConfig extends SensorConfig {
int cmd;
int subOpcode;

TauSensorConfig({
required this.cmd,
required this.subOpcode,
});

Uint8List toBytes() {
int randomByte = DateTime.now().microsecondsSinceEpoch & 0xFF;

return Uint8List.fromList([
0x00,
randomByte,
cmd,
subOpcode,
]);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'package:open_earable_flutter/src/managers/tau_sensor_handler.dart';

import '../sensor_configuration.dart';

class TauRingSensorConfiguration extends SensorConfiguration<TauRingSensorConfigurationValue> {

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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import 'dart:async';

import '../../../../managers/sensor_handler.dart';
import '../../sensor.dart';

class TauRingSensor extends Sensor<SensorIntValue> {
const TauRingSensor({
required this.sensorId,
required super.sensorName,
required super.chartTitle,
required super.shortChartTitle,
required List<String> axisNames,
required List<String> axisUnits,
required this.sensorHandler,
super.relatedConfigurations = const [],
}) : _axisNames = axisNames, _axisUnits = axisUnits;

final int sensorId;
final List<String> _axisNames;
final List<String> _axisUnits;

final SensorHandler sensorHandler;

@override
List<String> get axisNames => _axisNames;

@override
List<String> get axisUnits => _axisUnits;

@override
int get axisCount => _axisNames.length;

@override
Stream<SensorIntValue> get sensorStream {
StreamController<SensorIntValue> streamController = StreamController();
sensorHandler.subscribeToSensorData(sensorId).listen(
(data) {
int timestamp = data["timestamp"];

List<int> values = [];
for (var entry in (data[sensorName] as Map).entries) {
if (entry.key == 'units') {
continue;
}

values.add(entry.value);
}

SensorIntValue sensorValue = SensorIntValue(
values: values,
timestamp: timestamp,
);

streamController.add(sensorValue);
},
);
return streamController.stream;
}
}
68 changes: 68 additions & 0 deletions lib/src/models/devices/tau_ring.dart
Original file line number Diff line number Diff line change
@@ -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<Sensor> sensors = const [],
List<SensorConfiguration> sensorConfigs = const [],
required BleGattManager bleManager,
required super.disconnectNotifier,
}) : _sensors = sensors,
_sensorConfigs = sensorConfigs,
_bleManager = bleManager,
_discoveredDevice = discoveredDevice;

final DiscoveredDevice _discoveredDevice;

final List<Sensor> _sensors;
final List<SensorConfiguration> _sensorConfigs;
final BleGattManager _bleManager;

@override
final String deviceId;

@override
List<SensorConfiguration<SensorConfigurationValue>> get sensorConfigurations => _sensorConfigs;
@override
List<Sensor<SensorValue>> get sensors => _sensors;

@override
Future<void> disconnect() {
return _bleManager.disconnect(_discoveredDevice.id);
}

@override
Stream<Map<SensorConfiguration<SensorConfigurationValue>, 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<int> frame(int cmd, {List<int> payload = const [], int? rnd}) {
final r = rnd ?? DateTime.now().microsecondsSinceEpoch & 0xFF;
return [0x00, r & 0xFF, cmd, ...payload];
}

static List<int> le64(int ms) {
final b = List<int>.filled(8, 0);
var v = ms;
for (var i = 0; i < 8; i++) { b[i] = v & 0xFF; v >>= 8; }
return b;
}
}
77 changes: 77 additions & 0 deletions lib/src/models/devices/tau_ring_factory.dart
Original file line number Diff line number Diff line change
@@ -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<Wearable> createFromDevice(DiscoveredDevice device, {Set<ConnectionOption> 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<SensorConfiguration> sensorConfigs = [
TauRingSensorConfiguration(
name: "6-Axis IMU",
values: [
TauRingSensorConfigurationValue(key: "On", cmd: 0x40, subOpcode: 0x06),
TauRingSensorConfigurationValue(key: "Off", cmd: 0x40, subOpcode: 0x00),
],
sensorHandler: sensorHandler,
),
];
List<Sensor> 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<bool> matches(DiscoveredDevice device, List<BleService> services) async {
return services.any((s) => s.uuid.toLowerCase() == TauRingGatt.service);
}
}
Loading