Skip to content

Commit

Permalink
Replace nearby gear logic, add ota, fix scanning ui
Browse files Browse the repository at this point in the history
  • Loading branch information
Codel1417 committed Feb 22, 2024
1 parent 571066f commit ad0b978
Show file tree
Hide file tree
Showing 14 changed files with 536 additions and 220 deletions.
8 changes: 8 additions & 0 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@
<string>Need Location permission</string>
<key>NSMotionUsageDescription</key>
<string>This application tracks your steps</string>
<key>NSBonjourServices</key>
<array>
<string>_tailapp._tcp</string>
</array>
<key>UIRequiresPersistentWiFi</key>
<true/>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>{YOUR_DESCRIPTION}</string>
<key>UIBackgroundModes</key>
<array>
<string>processing</string>
Expand Down
40 changes: 38 additions & 2 deletions lib/Backend/Bluetooth/BluetoothManager.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';

import 'package:collection/collection.dart';
import 'package:cross_platform/cross_platform.dart';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_service/flutter_foreground_service.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
Expand All @@ -15,10 +16,13 @@ import 'package:tail_app/Frontend/Widgets/snack_bar_overlay.dart';

import '../Definitions/Device/BaseDeviceDefinition.dart';
import '../DeviceRegistry.dart';
import '../FirmwareUpdate.dart';
import 'btMessage.dart';

part 'BluetoothManager.g.dart';

Dio dio = Dio();

@Riverpod(dependencies: [reactiveBLE, KnownDevices])
Stream<DiscoveredDevice> scanForDevices(ScanForDevicesRef ref) {
Flogger.d("Starting scan");
Expand Down Expand Up @@ -75,7 +79,7 @@ class KnownDevices extends _$KnownDevices {

Future<void> connect(DiscoveredDevice device) async {
final ISentrySpan transaction = Sentry.startTransaction('connectToDevice()', 'task');
BaseDeviceDefinition? deviceDefinition = DeviceRegistry.getByService(device.serviceUuids);
BaseDeviceDefinition? deviceDefinition = DeviceRegistry.getByName(device.name);
if (deviceDefinition == null) {
Flogger.w("Unknown device found: ${device.name}");
transaction.status = const SpanStatus.notFound();
Expand Down Expand Up @@ -113,17 +117,27 @@ class KnownDevices extends _$KnownDevices {
Flogger.i("Received message from ${baseStoredDevice.name}: $value");
if (value.startsWith("VER")) {
statefulDevice.fwVersion.value = value.substring(value.indexOf(" "));
if (statefulDevice.fwInfo.value != null) {
if (statefulDevice.fwInfo.value?.version.split(" ")[1] != statefulDevice.fwVersion.value) {
statefulDevice.hasUpdate.value = true;
}
}
} else if (value.startsWith("GLOWTIP")) {
statefulDevice.glowTip.value = "TRUE" == value.substring(value.indexOf(" "));
} else if (value.contains("BUSY")) {
statefulDevice.deviceState.value = DeviceState.busy;
//statefulDevice.deviceState.value = DeviceState.busy;
//TODO: add busy check to see if gear ready for next command
}
});
statefulDevice.batteryCharacteristicStreamSubscription = reactiveBLE.subscribeToCharacteristic(statefulDevice.batteryCharacteristic).listen((List<int> event) {
Flogger.d("Received Battery message from ${baseStoredDevice.name}: $event");
statefulDevice.battery.value = event.first.toDouble();
});
statefulDevice.batteryChargeCharacteristicStreamSubscription = reactiveBLE.subscribeToCharacteristic(statefulDevice.batteryChargeCharacteristic).listen((List<int> event) {
String value = const Utf8Decoder().convert(event);
Flogger.d("Received Battery Charge message from ${baseStoredDevice.name}: $value");
statefulDevice.batteryCharging.value = value == "CHARGE ON";
});
statefulDevice.keepAliveStreamSubscription = Stream.periodic(const Duration(seconds: 15)).listen((event) async {
if (state.containsKey(device.id)) {
statefulDevice.commandQueue.addCommand(BluetoothMessage("PING", statefulDevice, Priority.low));
Expand All @@ -132,6 +146,21 @@ class KnownDevices extends _$KnownDevices {
throw Exception("Disconnected from device");
}
}, cancelOnError: true);
if (deviceDefinition.fwURL != "") {
dio.get(statefulDevice.baseDeviceDefinition.fwURL, options: Options(responseType: ResponseType.json)).then(
(value) {
if (value.statusCode == 200) {
statefulDevice.fwInfo.value = FWInfo.fromJson(const JsonDecoder().convert(value.data.toString()));
if (statefulDevice.fwVersion.value != "") {
if (statefulDevice.fwInfo.value?.version.split(" ")[1] != statefulDevice.fwVersion.value) {
statefulDevice.hasUpdate.value = true;
}
}
}
},
).onError((error, stackTrace) => Flogger.e("Unable to get Firmware info for ${statefulDevice.baseDeviceDefinition.fwURL} :$error", stackTrace: stackTrace));
}
statefulDevice.commandQueue.addCommand(BluetoothMessage("VER", statefulDevice, Priority.low));
}
});
transaction.status = const SpanStatus.ok();
Expand Down Expand Up @@ -172,6 +201,13 @@ StreamSubscription<ConnectionStateUpdate> btConnectStateHandler(BtConnectStateHa
knownDevices[event.deviceId]?.keepAliveStreamSubscription = null;
knownDevices[event.deviceId]?.battery.value = -1;
knownDevices[event.deviceId]?.rssi.value = -1;
knownDevices[event.deviceId]?.hasUpdate.value = false;
knownDevices[event.deviceId]?.fwInfo.value = null;
knownDevices[event.deviceId]?.fwVersion.value = "";
knownDevices[event.deviceId]?.batteryCharging.value = false;
knownDevices[event.deviceId]?.batteryChargeCharacteristicStreamSubscription?.cancel();
knownDevices[event.deviceId]?.batteryChargeCharacteristicStreamSubscription = null;

ref.read(snackbarStreamProvider.notifier).add(SnackBar(content: Text("Disconnected from ${knownDevices[event.deviceId]?.baseStoredDevice.name}")));
//remove foreground service if no devices connected
if (Platform.isAndroid && knownDevices.values.where((element) => element.deviceConnectionState.value == DeviceConnectionState.connected).isEmpty) {
Expand Down
16 changes: 11 additions & 5 deletions lib/Backend/Definitions/Device/BaseDeviceDefinition.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:tail_app/Backend/Bluetooth/BluetoothManager.dart';
import 'package:tail_app/Backend/FirmwareUpdate.dart';

import '../../../Frontend/intnDefs.dart';

Expand Down Expand Up @@ -49,15 +50,14 @@ enum DeviceState { standby, runAction, busy }

class BaseDeviceDefinition {
final String uuid;
final String model;
final String btName;
final Uuid bleDeviceService;
final Uuid bleRxCharacteristic;
final Uuid bleTxCharacteristic;
final Icon icon;
final DeviceType deviceType;
final String fwURL;

const BaseDeviceDefinition(this.uuid, this.model, this.btName, this.bleDeviceService, this.bleRxCharacteristic, this.bleTxCharacteristic, this.icon, this.deviceType);
const BaseDeviceDefinition(this.uuid, this.btName, this.bleDeviceService, this.bleRxCharacteristic, this.bleTxCharacteristic, this.deviceType, this.fwURL);

@override
String toString() {
Expand All @@ -72,8 +72,10 @@ class BaseStatefulDevice {
late QualifiedCharacteristic rxCharacteristic;
late QualifiedCharacteristic txCharacteristic;
late QualifiedCharacteristic batteryCharacteristic;
late QualifiedCharacteristic otaCharacteristic;
late QualifiedCharacteristic batteryChargeCharacteristic;

ValueNotifier<double> battery = ValueNotifier(-1);
ValueNotifier<bool> batteryCharging = ValueNotifier(false);
ValueNotifier<String> fwVersion = ValueNotifier("");
ValueNotifier<bool> glowTip = ValueNotifier(false);
StreamSubscription<ConnectionStateUpdate>? connectionStateStreamSubscription;
Expand All @@ -84,6 +86,8 @@ class BaseStatefulDevice {
Stream<List<int>>? get rxCharacteristicStream => _rxCharacteristicStream;
ValueNotifier<DeviceConnectionState> deviceConnectionState = ValueNotifier(DeviceConnectionState.disconnected);
ValueNotifier<int> rssi = ValueNotifier(-1);
ValueNotifier<FWInfo?> fwInfo = ValueNotifier(null);
ValueNotifier<bool> hasUpdate = ValueNotifier(false);

set rxCharacteristicStream(Stream<List<int>>? value) {
_rxCharacteristicStream = value?.asBroadcastStream();
Expand All @@ -92,13 +96,15 @@ class BaseStatefulDevice {
Ref? ref;
late CommandQueue commandQueue;
StreamSubscription<List<int>>? batteryCharacteristicStreamSubscription;
StreamSubscription<List<int>>? batteryChargeCharacteristicStreamSubscription;

BaseStatefulDevice(this.baseDeviceDefinition, this.baseStoredDevice, this.ref) {
rxCharacteristic = QualifiedCharacteristic(characteristicId: baseDeviceDefinition.bleRxCharacteristic, serviceId: baseDeviceDefinition.bleDeviceService, deviceId: baseStoredDevice.btMACAddress);
txCharacteristic = QualifiedCharacteristic(characteristicId: baseDeviceDefinition.bleTxCharacteristic, serviceId: baseDeviceDefinition.bleDeviceService, deviceId: baseStoredDevice.btMACAddress);
batteryCharacteristic = QualifiedCharacteristic(serviceId: Uuid.parse("0000180f-0000-1000-8000-00805f9b34fb"), characteristicId: Uuid.parse("00002a19-0000-1000-8000-00805f9b34fb"), deviceId: baseStoredDevice.btMACAddress);
batteryChargeCharacteristic = QualifiedCharacteristic(serviceId: Uuid.parse("0000180f-0000-1000-8000-00805f9b34fb"), characteristicId: Uuid.parse("5073792e-4fc0-45a0-b0a5-78b6c1756c91"), deviceId: baseStoredDevice.btMACAddress);

commandQueue = CommandQueue(ref, this);
otaCharacteristic = QualifiedCharacteristic(characteristicId: Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"), serviceId: Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"), deviceId: baseStoredDevice.btMACAddress);
}

@override
Expand Down
60 changes: 56 additions & 4 deletions lib/Backend/DeviceRegistry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,68 @@ part 'DeviceRegistry.g.dart';
@immutable
class DeviceRegistry {
static Set<BaseDeviceDefinition> allDevices = {
BaseDeviceDefinition("798e1528-2832-4a87-93d7-4d1b25a2f418", "MiTail", "MiTail", Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"), Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"), Uuid.parse("c6612b64-0087-4974-939e-68968ef294b0"), const Icon(Icons.bluetooth), DeviceType.tail),
BaseDeviceDefinition("927dee04-ddd4-4582-8e42-69dc9fbfae66", "EG2", "EG2", Uuid.parse("927dee04-ddd4-4582-8e42-69dc9fbfae66"), Uuid.parse("0b646a19-371e-4327-b169-9632d56c0e84"), Uuid.parse("05e026d8-b395-4416-9f8a-c00d6c3781b9"), const Icon(Icons.bluetooth), DeviceType.ears)
BaseDeviceDefinition(
"798e1528-2832-4a87-93d7-4d1b25a2f418",
"MiTail",
Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"),
Uuid.parse("c6612b64-0087-4974-939e-68968ef294b0"),
Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"),
DeviceType.tail,
"https://thetailcompany.com/fw/mitail",
),
BaseDeviceDefinition(
"9c5f3692-1c6e-4d46-b607-4f6f4a6e28ee",
"(!)Tail1",
Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"),
Uuid.parse("c6612b64-0087-4974-939e-68968ef294b0"),
Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"),
DeviceType.tail,
"",
),
BaseDeviceDefinition(
"5fb21175-fef4-448a-a38b-c472d935abab",
"minitail",
Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"),
Uuid.parse("c6612b64-0087-4974-939e-68968ef294b0"),
Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"),
DeviceType.tail,
"https://thetailcompany.com/fw/mini",
),
BaseDeviceDefinition(
"e790f509-f95b-4eb4-b649-5b43ee1eee9c",
"flutter",
Uuid.parse("3af2108b-d066-42da-a7d4-55648fa0a9b6"),
Uuid.parse("c6612b64-0087-4974-939e-68968ef294b0"),
Uuid.parse("5bfd6484-ddee-4723-bfe6-b653372bbfd6"),
DeviceType.wings,
"https://thetailcompany.com/fw/flutter",
),
BaseDeviceDefinition(
"927dee04-ddd4-4582-8e42-69dc9fbfae66",
"EG2",
Uuid.parse("927dee04-ddd4-4582-8e42-69dc9fbfae66"),
Uuid.parse("0b646a19-371e-4327-b169-9632d56c0e84"),
Uuid.parse("05e026d8-b395-4416-9f8a-c00d6c3781b9"),
DeviceType.ears,
"https://thetailcompany.com/fw/eg",
),
BaseDeviceDefinition(
"ba2f2b00-8f65-4cc3-afad-58ba1fccd62d",
"EarGear",
Uuid.parse("927dee04-ddd4-4582-8e42-69dc9fbfae66"),
Uuid.parse("0b646a19-371e-4327-b169-9632d56c0e84"),
Uuid.parse("05e026d8-b395-4416-9f8a-c00d6c3781b9"),
DeviceType.ears,
"",
),
};

static BaseDeviceDefinition getByUUID(String uuid) {
return allDevices.firstWhere((BaseDeviceDefinition element) => element.uuid == uuid);
}

static BaseDeviceDefinition getByName(String id) {
return allDevices.firstWhere((BaseDeviceDefinition element) => element.btName == id);
static BaseDeviceDefinition? getByName(String id) {
return allDevices.firstWhere((BaseDeviceDefinition element) => element.btName.toLowerCase() == id.toLowerCase());
}

static bool hasByName(String id) {
Expand Down
18 changes: 18 additions & 0 deletions lib/Backend/FirmwareUpdate.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import 'package:json_annotation/json_annotation.dart';

part 'FirmwareUpdate.g.dart';

@JsonSerializable()
class FWInfo {
String version;
String md5sum;
String url;
String changelog;
String glash;

FWInfo(this.version, this.md5sum, this.url, this.changelog, this.glash);

factory FWInfo.fromJson(Map<String, dynamic> json) => _$FWInfoFromJson(json);

Map<String, dynamic> toJson() => _$FWInfoToJson(this);
}
1 change: 0 additions & 1 deletion lib/Backend/NavigationObserver/CustomNavObserver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ class CustomNavObserver extends NavigatorObserver {
String? name = route.settings.name;
String refferalName = previousRoute?.settings.name ?? "";
if (name != null) {
plausible.screenWidth = MediaQuery.of(route.navigator!.context).size.width.toString();
plausible.event(page: route.settings.name.toString(), props: {"Number Of Devices": SentryHive.box<BaseStoredDevice>('devices').length.toString()}, referrer: refferalName);
}
}
Expand Down
33 changes: 23 additions & 10 deletions lib/Backend/Sensors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_android_volume_keydown/flutter_android_volume_keydown.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:flutter_nearby_connections/flutter_nearby_connections.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive/hive.dart';
import 'package:logging_flutter/logging_flutter.dart';
Expand All @@ -19,13 +19,13 @@ import '../Frontend/intnDefs.dart';
import 'Bluetooth/BluetoothManager.dart';
import 'Definitions/Action/BaseAction.dart';
import 'Definitions/Device/BaseDeviceDefinition.dart';
import 'DeviceRegistry.dart';
import 'moveLists.dart';

part 'Sensors.g.dart';

//TODO: wrap EarGear Mic and Tilt to Sensors, send enable/disable commands with toggle
//TODO: error callback to disable the sensor from the trigger definition, such as when permission is denied
//TODO: Call disable method when last device reconnects, call enable method when first device connects
@HiveType(typeId: 2)
class Trigger {
@HiveField(1)
Expand Down Expand Up @@ -70,7 +70,7 @@ class Trigger {
}

@HiveField(3)
List<TriggerAction> actions = []; //TODO: Store action as a uuid, and find on demand
List<TriggerAction> actions = [];

Trigger(this.triggerDef) {
// called by hive when loading object
Expand Down Expand Up @@ -157,10 +157,10 @@ class WalkingTriggerDefinition extends TriggerDefinition {
pedestrianStatusStream = Pedometer.pedestrianStatusStream.listen(
(PedestrianStatus event) {
Flogger.i("PedestrianStatus:: ${event.status}");
if (event.status == "Walking") {
if (event.status == "walking") {
TriggerAction? action = actions.firstWhere((element) => actionTypes.firstWhere((element) => element.name == "Walking").uuid == element.uuid);
sendCommands(deviceType, action.action, ref);
} else if (event.status == "Stopped") {
} else if (event.status == "stopped") {
TriggerAction? action = actions.firstWhere((element) => actionTypes.firstWhere((element) => element.name == "Stopped").uuid == element.uuid);
sendCommands(deviceType, action.action, ref);
}
Expand Down Expand Up @@ -278,7 +278,8 @@ class ShakeTriggerDefinition extends TriggerDefinition {
}

class TailProximityTriggerDefinition extends TriggerDefinition {
StreamSubscription<DiscoveredDevice>? btStream;
StreamSubscription? subscription;
NearbyService? nearbyService;

TailProximityTriggerDefinition(super.ref) {
super.name = triggerProximityTitle();
Expand All @@ -290,14 +291,26 @@ class TailProximityTriggerDefinition extends TriggerDefinition {

@override
Future<void> onDisable() async {
btStream?.cancel();
btStream = null;
subscription?.cancel();
subscription = null;
await nearbyService?.stopAdvertisingPeer();
await nearbyService?.stopBrowsingForPeers();
}

@override
Future<void> onEnable(Set<TriggerAction> actions, Set<DeviceType> deviceType) async {
btStream = ref.read(reactiveBLEProvider).scanForDevices(withServices: DeviceRegistry.getAllIds()).where((event) => !ref.read(knownDevicesProvider).keys.contains(event.id)).listen((DiscoveredDevice device) {
Flogger.d("TailProximityTriggerDefinition:: $device");
nearbyService = NearbyService();
await nearbyService?.init(
serviceType: "tailapp",
strategy: Strategy.P2P_POINT_TO_POINT,
callback: (isRunning) async {
if (isRunning) {
await nearbyService?.startAdvertisingPeer();
await nearbyService?.startBrowsingForPeers();
}
});
subscription = nearbyService?.stateChangedSubscription(callback: (devicesList) {
Flogger.d("TailProximityTriggerDefinition::");
TriggerAction? action = actions.firstWhere((element) => actionTypes.firstWhere((element) => element.name == "Nearby Gear").uuid == element.uuid);
sendCommands(deviceType, action.action, ref);
});
Expand Down
2 changes: 1 addition & 1 deletion lib/Frontend/Widgets/manage_devices.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class ManageDevices extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
children: const [
children: [
ManageKnownDevices(),
ScanForNewDevice(),
],
Expand Down
Loading

0 comments on commit ad0b978

Please sign in to comment.