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
6 changes: 3 additions & 3 deletions open_wearable/lib/apps/widgets/apps_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ List<AppInfo> _apps = [
widget: SelectEarableView(startApp: (wearable, sensorConfigProvider) {
return PostureTrackerView(
EarableAttitudeTracker(
wearable as SensorManager,
wearable.requireCapability<SensorManager>(),
sensorConfigProvider,
wearable.name.endsWith("L"),
),
Expand All @@ -45,9 +45,9 @@ List<AppInfo> _apps = [
description: "Track your heart rate and other vitals",
widget: SelectEarableView(
startApp: (wearable, _) {
if (wearable is SensorManager) {
if (wearable.hasCapability<SensorManager>()) {
//TODO: show alert if no ppg sensor is found
Sensor ppgSensor = (wearable as SensorManager).sensors.firstWhere(
Sensor ppgSensor = wearable.requireCapability<SensorManager>().sensors.firstWhere(
(s) => s.sensorName.toLowerCase() == "photoplethysmography".toLowerCase(),
);

Expand Down
4 changes: 2 additions & 2 deletions open_wearable/lib/view_models/sensor_recorder_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ class SensorRecorderProvider with ChangeNotifier {
notifyListeners();
});

if (wearable is SensorManager) {
for (Sensor sensor in (wearable as SensorManager).sensors) {
if (wearable.hasCapability<SensorManager>()) {
for (Sensor sensor in wearable.requireCapability<SensorManager>().sensors) {
if (!_recorders[wearable]!.containsKey(sensor)) {
_recorders[wearable]![sensor] = Recorder(columns: sensor.axisNames);
}
Expand Down
153 changes: 109 additions & 44 deletions open_wearable/lib/view_models/wearables_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class WearableTimeSynchronizedEvent extends WearableEvent {
WearableTimeSynchronizedEvent({
required super.wearable,
String? description,
}): super(description: description ?? 'Time synchronized for ${wearable.name}');
}) : super(description: description ?? 'Time synchronized for ${wearable.name}');

@override
String toString() => 'WearableTimeSynchronizedEvent for ${wearable.name}';
Expand All @@ -65,13 +65,16 @@ class WearableErrorEvent extends WearableEvent {
required super.wearable,
required this.errorMessage,
String? description,
}): super(description: description ?? 'Error for ${wearable.name}: $errorMessage');
}) : super(description: description ?? 'Error for ${wearable.name}: $errorMessage');

@override
String toString() =>
'WearableErrorEvent for ${wearable.name}: $errorMessage, description: $description';
}


// MARK: WearablesProvider

class WearablesProvider with ChangeNotifier {
final List<Wearable> _wearables = [];
final Map<Wearable, SensorConfigurationProvider>
Expand All @@ -90,43 +93,95 @@ class WearablesProvider with ChangeNotifier {
StreamController<WearableEvent>.broadcast();
Stream<WearableEvent> get wearableEventStream => _wearableEventController.stream;

void addWearable(Wearable wearable) {
// 1) Fast path: ignore duplicates and push into lists/maps synchronously
if (_wearables.any((w) => w.deviceId == wearable.deviceId)) {
return;
}
final Map<Wearable, StreamSubscription> _capabilitySubscriptions = {};

// MARK: Internal helpers

if (wearable is TimeSynchronizable) {
bool _isDuplicateDevice(Wearable wearable) =>
_wearables.any((w) => w.deviceId == wearable.deviceId);

void _emitWearableEvent(WearableEvent event) {
_wearableEventController.add(event);
}

void _emitWearableError({
required Wearable wearable,
required String errorMessage,
String? description,
}) {
_emitWearableEvent(
WearableErrorEvent(
wearable: wearable,
errorMessage: errorMessage,
description: description,
),
);
}

void _scheduleMicrotask(FutureOr<void> Function() work) {
Future.microtask(() async {
try {
await work();
} catch (e, st) {
logger.w('WearablesProvider microtask failed: $e\n$st');
}
});
}

Future<void> _syncTimeAndEmit({
required Wearable wearable,
required String successDescription,
required String failureDescription,
}) async {
try {
logger.d('Synchronizing time for wearable ${wearable.name}');
(wearable as TimeSynchronizable).synchronizeTime().then((_) {
logger.d('Time synchronized for wearable ${wearable.name}');
_wearableEventController.add(WearableTimeSynchronizedEvent(wearable: wearable, description: 'Time synchronized for ${wearable.name}'));
}).catchError((e, st) {
logger.w('Failed to synchronize time for wearable ${wearable.name}: $e\n$st');
_wearableEventController.add(
WearableErrorEvent(
wearable: wearable,
errorMessage: 'Failed to synchronize time with ${wearable.name}: $e',
description: 'Failed to synchronize time for ${wearable.name}',
),
);
});
await (wearable.requireCapability<TimeSynchronizable>()).synchronizeTime();
logger.d('Time synchronized for wearable ${wearable.name}');
_emitWearableEvent(
WearableTimeSynchronizedEvent(
wearable: wearable,
description: successDescription,
),
);
} catch (e, st) {
logger.w('Failed to synchronize time for wearable ${wearable.name}: $e\n$st');
_emitWearableError(
wearable: wearable,
errorMessage: 'Failed to synchronize time with ${wearable.name}: $e',
description: failureDescription,
);
}
}

void addWearable(Wearable wearable) {
// 1) Fast path: ignore duplicates and push into lists/maps synchronously
if (_isDuplicateDevice(wearable)) return;

_wearables.add(wearable);

_capabilitySubscriptions[wearable] = wearable.capabilityRegistered.listen((addedCapabilities) {
_handleCapabilitiesChanged(wearable: wearable, addedCapabilites: addedCapabilities);
});

// Init SensorConfigurationProvider synchronously (no awaits here)
if (wearable is SensorConfigurationManager) {
if (wearable.hasCapability<SensorConfigurationManager>()) {
_ensureSensorConfigProvider(wearable);
final notifier = _sensorConfigurationProviders[wearable]!;
for (final config
in (wearable as SensorConfigurationManager).sensorConfigurations) {
in (wearable.requireCapability<SensorConfigurationManager>()).sensorConfigurations) {
if (notifier.getSelectedConfigurationValue(config) == null &&
config.values.isNotEmpty) {
notifier.addSensorConfiguration(config, config.values.first);
}
}
}
if (wearable.hasCapability<TimeSynchronizable>()) {
_scheduleMicrotask(() => _syncTimeAndEmit(
wearable: wearable,
successDescription: 'Time synchronized for ${wearable.name}',
failureDescription: 'Failed to synchronize time for ${wearable.name}',
),);
}

// Disconnect listener (sync)
wearable.addDisconnectListener(() {
Expand All @@ -139,35 +194,27 @@ class WearablesProvider with ChangeNotifier {

// 2) Slow/async work: run in microtasks so it doesn't block the add
// Stereo pairing (if applicable)
if (wearable is StereoDevice) {
Future.microtask(
() => _maybeAutoPairStereoAsync(wearable as StereoDevice),
);
if (wearable.hasCapability<StereoDevice>()) {
_scheduleMicrotask(() => _maybeAutoPairStereoAsync(wearable.requireCapability<StereoDevice>()));
}

// Firmware support check (if applicable)
if (wearable is DeviceFirmwareVersion) {
Future.microtask(
() => _maybeEmitUnsupportedFirmwareAsync(
wearable as DeviceFirmwareVersion,
),
);
if (wearable.hasCapability<DeviceFirmwareVersion>()) {
_scheduleMicrotask(() => _maybeEmitUnsupportedFirmwareAsync(wearable.requireCapability<DeviceFirmwareVersion>()));
}

// Check for newer firmware (if applicable)
if (wearable is DeviceFirmwareVersion) {
Future.microtask(
() => _checkForNewerFirmwareAsync(wearable as DeviceFirmwareVersion),
);
if (wearable.hasCapability<DeviceFirmwareVersion>()) {
_scheduleMicrotask(() => _checkForNewerFirmwareAsync(wearable.requireCapability<DeviceFirmwareVersion>()));
}
}

// --- Helpers ---------------------------------------------------------------
// MARK: Helpers

void _ensureSensorConfigProvider(Wearable wearable) {
if (!_sensorConfigurationProviders.containsKey(wearable)) {
_sensorConfigurationProviders[wearable] = SensorConfigurationProvider(
sensorConfigurationManager: wearable as SensorConfigurationManager,
sensorConfigurationManager: wearable.requireCapability<SensorConfigurationManager>(),
);
}
}
Expand Down Expand Up @@ -202,6 +249,7 @@ class WearablesProvider with ChangeNotifier {
DeviceFirmwareVersion dev,
) async {
try {
final wearable = dev as Wearable;
// In your abstraction, isFirmwareSupported is a Future<bool> getter.
final supportStatus = await dev.checkFirmwareSupport();
switch (supportStatus) {
Expand All @@ -210,21 +258,22 @@ class WearablesProvider with ChangeNotifier {
break;
case FirmwareSupportStatus.tooNew:
_unsupportedFirmwareEventsController
.add(FirmwareTooNewEvent(dev as Wearable));
.add(FirmwareTooNewEvent(wearable));
break;
case FirmwareSupportStatus.unsupported:
_unsupportedFirmwareEventsController
.add(FirmwareUnsupportedEvent(dev as Wearable));
.add(FirmwareUnsupportedEvent(wearable));
break;
case FirmwareSupportStatus.tooOld:
_unsupportedFirmwareEventsController
.add(FirmwareTooOldEvent(dev as Wearable));
.add(FirmwareTooOldEvent(wearable));
case FirmwareSupportStatus.unknown:
logger.w('Firmware support unknown for ${(dev as Wearable).name}');
logger.w('Firmware support unknown for ${wearable.name}');
break;
}
} catch (e, st) {
logger.w('Firmware check failed for ${(dev as Wearable).name}: $e\n$st');
final wearable = dev as Wearable;
logger.w('Firmware check failed for ${wearable.name}: $e\n$st');
}
}

Expand Down Expand Up @@ -274,6 +323,7 @@ class WearablesProvider with ChangeNotifier {
void removeWearable(Wearable wearable) {
_wearables.remove(wearable);
_sensorConfigurationProviders.remove(wearable);
_capabilitySubscriptions.remove(wearable)?.cancel();
notifyListeners();
}

Expand All @@ -287,4 +337,19 @@ class WearablesProvider with ChangeNotifier {
}
return _sensorConfigurationProviders[wearable]!;
}

void _handleCapabilitiesChanged({required Wearable wearable, required List<Type> addedCapabilites}) {
if (addedCapabilites.contains(SensorConfigurationManager)) {
_ensureSensorConfigProvider(wearable);
}
if (addedCapabilites.contains(TimeSynchronizable)) {
_scheduleMicrotask(() => _syncTimeAndEmit(
wearable: wearable,
successDescription:
'Time synchronized for ${wearable.name} after capability change',
failureDescription:
'Failed to synchronize time for ${wearable.name} after capability change',
),);
}
}
}
12 changes: 6 additions & 6 deletions open_wearable/lib/widgets/devices/battery_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ class BatteryStateView extends StatelessWidget {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
if (_device is BatteryLevelStatus)
if (_device.hasCapability<BatteryLevelStatus>())
StreamBuilder(
stream: (_device as BatteryLevelStatus).batteryPercentageStream,
stream: _device.requireCapability<BatteryLevelStatus>().batteryPercentageStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return PlatformText("${snapshot.data}%");
Expand All @@ -24,9 +24,9 @@ class BatteryStateView extends StatelessWidget {
}
},
),
if (_device is BatteryLevelStatusService)
if (_device.hasCapability<BatteryLevelStatusService>())
StreamBuilder(
stream: (_device as BatteryLevelStatusService).powerStatusStream,
stream: _device.requireCapability<BatteryLevelStatusService>().powerStatusStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
if (!snapshot.data!.batteryPresent) {
Expand All @@ -52,9 +52,9 @@ class BatteryStateView extends StatelessWidget {
}
},
)
else if (_device is BatteryLevelStatus)
else if (_device.hasCapability<BatteryLevelStatus>())
StreamBuilder(
stream: (_device as BatteryLevelStatus).batteryPercentageStream,
stream: _device.requireCapability<BatteryLevelStatus>().batteryPercentageStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Icon(getBatteryIcon(snapshot.data!));
Expand Down
Loading