From ad0b978b7681c18bf21fef5996de461495655f00 Mon Sep 17 00:00:00 2001
From: Codel1417 <13484789+Codel1417@users.noreply.github.com>
Date: Thu, 22 Feb 2024 16:48:11 -0500
Subject: [PATCH] Replace nearby gear logic, add ota, fix scanning ui
---
ios/Runner/Info.plist | 8 +
lib/Backend/Bluetooth/BluetoothManager.dart | 40 +-
.../Device/BaseDeviceDefinition.dart | 16 +-
lib/Backend/DeviceRegistry.dart | 60 ++-
lib/Backend/FirmwareUpdate.dart | 18 +
.../NavigationObserver/CustomNavObserver.dart | 1 -
lib/Backend/Sensors.dart | 33 +-
lib/Frontend/Widgets/manage_devices.dart | 2 +-
lib/Frontend/Widgets/scan_for_new_device.dart | 40 +-
lib/Frontend/pages/Shell.dart | 380 ++++++++++--------
lib/Frontend/pages/ota_update.dart | 99 +++++
lib/main.dart | 14 +-
pubspec.lock | 38 +-
pubspec.yaml | 7 +-
14 files changed, 536 insertions(+), 220 deletions(-)
create mode 100644 lib/Backend/FirmwareUpdate.dart
create mode 100644 lib/Frontend/pages/ota_update.dart
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 97e35aee..0595ccd8 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -59,6 +59,14 @@
Need Location permission
NSMotionUsageDescription
This application tracks your steps
+ NSBonjourServices
+
+ _tailapp._tcp
+
+ UIRequiresPersistentWiFi
+
+ NSBluetoothAlwaysUsageDescription
+ {YOUR_DESCRIPTION}
UIBackgroundModes
processing
diff --git a/lib/Backend/Bluetooth/BluetoothManager.dart b/lib/Backend/Bluetooth/BluetoothManager.dart
index 40b2d41d..aac219dc 100644
--- a/lib/Backend/Bluetooth/BluetoothManager.dart
+++ b/lib/Backend/Bluetooth/BluetoothManager.dart
@@ -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';
@@ -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 scanForDevices(ScanForDevicesRef ref) {
Flogger.d("Starting scan");
@@ -75,7 +79,7 @@ class KnownDevices extends _$KnownDevices {
Future 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();
@@ -113,10 +117,15 @@ 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
}
});
@@ -124,6 +133,11 @@ class KnownDevices extends _$KnownDevices {
Flogger.d("Received Battery message from ${baseStoredDevice.name}: $event");
statefulDevice.battery.value = event.first.toDouble();
});
+ statefulDevice.batteryChargeCharacteristicStreamSubscription = reactiveBLE.subscribeToCharacteristic(statefulDevice.batteryChargeCharacteristic).listen((List 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));
@@ -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();
@@ -172,6 +201,13 @@ StreamSubscription 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) {
diff --git a/lib/Backend/Definitions/Device/BaseDeviceDefinition.dart b/lib/Backend/Definitions/Device/BaseDeviceDefinition.dart
index c855b8ee..3d621b18 100644
--- a/lib/Backend/Definitions/Device/BaseDeviceDefinition.dart
+++ b/lib/Backend/Definitions/Device/BaseDeviceDefinition.dart
@@ -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';
@@ -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() {
@@ -72,8 +72,10 @@ class BaseStatefulDevice {
late QualifiedCharacteristic rxCharacteristic;
late QualifiedCharacteristic txCharacteristic;
late QualifiedCharacteristic batteryCharacteristic;
- late QualifiedCharacteristic otaCharacteristic;
+ late QualifiedCharacteristic batteryChargeCharacteristic;
+
ValueNotifier battery = ValueNotifier(-1);
+ ValueNotifier batteryCharging = ValueNotifier(false);
ValueNotifier fwVersion = ValueNotifier("");
ValueNotifier glowTip = ValueNotifier(false);
StreamSubscription? connectionStateStreamSubscription;
@@ -84,6 +86,8 @@ class BaseStatefulDevice {
Stream>? get rxCharacteristicStream => _rxCharacteristicStream;
ValueNotifier deviceConnectionState = ValueNotifier(DeviceConnectionState.disconnected);
ValueNotifier rssi = ValueNotifier(-1);
+ ValueNotifier fwInfo = ValueNotifier(null);
+ ValueNotifier hasUpdate = ValueNotifier(false);
set rxCharacteristicStream(Stream>? value) {
_rxCharacteristicStream = value?.asBroadcastStream();
@@ -92,13 +96,15 @@ class BaseStatefulDevice {
Ref? ref;
late CommandQueue commandQueue;
StreamSubscription>? batteryCharacteristicStreamSubscription;
+ StreamSubscription>? 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
diff --git a/lib/Backend/DeviceRegistry.dart b/lib/Backend/DeviceRegistry.dart
index f4e32625..8152244c 100644
--- a/lib/Backend/DeviceRegistry.dart
+++ b/lib/Backend/DeviceRegistry.dart
@@ -11,16 +11,68 @@ part 'DeviceRegistry.g.dart';
@immutable
class DeviceRegistry {
static Set 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) {
diff --git a/lib/Backend/FirmwareUpdate.dart b/lib/Backend/FirmwareUpdate.dart
new file mode 100644
index 00000000..0b7171b9
--- /dev/null
+++ b/lib/Backend/FirmwareUpdate.dart
@@ -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 json) => _$FWInfoFromJson(json);
+
+ Map toJson() => _$FWInfoToJson(this);
+}
diff --git a/lib/Backend/NavigationObserver/CustomNavObserver.dart b/lib/Backend/NavigationObserver/CustomNavObserver.dart
index e7913a48..01031dcc 100644
--- a/lib/Backend/NavigationObserver/CustomNavObserver.dart
+++ b/lib/Backend/NavigationObserver/CustomNavObserver.dart
@@ -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('devices').length.toString()}, referrer: refferalName);
}
}
diff --git a/lib/Backend/Sensors.dart b/lib/Backend/Sensors.dart
index 182a2c9c..b4fb3229 100644
--- a/lib/Backend/Sensors.dart
+++ b/lib/Backend/Sensors.dart
@@ -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';
@@ -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)
@@ -70,7 +70,7 @@ class Trigger {
}
@HiveField(3)
- List actions = []; //TODO: Store action as a uuid, and find on demand
+ List actions = [];
Trigger(this.triggerDef) {
// called by hive when loading object
@@ -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);
}
@@ -278,7 +278,8 @@ class ShakeTriggerDefinition extends TriggerDefinition {
}
class TailProximityTriggerDefinition extends TriggerDefinition {
- StreamSubscription? btStream;
+ StreamSubscription? subscription;
+ NearbyService? nearbyService;
TailProximityTriggerDefinition(super.ref) {
super.name = triggerProximityTitle();
@@ -290,14 +291,26 @@ class TailProximityTriggerDefinition extends TriggerDefinition {
@override
Future onDisable() async {
- btStream?.cancel();
- btStream = null;
+ subscription?.cancel();
+ subscription = null;
+ await nearbyService?.stopAdvertisingPeer();
+ await nearbyService?.stopBrowsingForPeers();
}
@override
Future onEnable(Set actions, Set 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);
});
diff --git a/lib/Frontend/Widgets/manage_devices.dart b/lib/Frontend/Widgets/manage_devices.dart
index 1b0dbe99..84466102 100644
--- a/lib/Frontend/Widgets/manage_devices.dart
+++ b/lib/Frontend/Widgets/manage_devices.dart
@@ -8,7 +8,7 @@ class ManageDevices extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView(
- children: const [
+ children: [
ManageKnownDevices(),
ScanForNewDevice(),
],
diff --git a/lib/Frontend/Widgets/scan_for_new_device.dart b/lib/Frontend/Widgets/scan_for_new_device.dart
index 3fb07890..034c3bc7 100644
--- a/lib/Frontend/Widgets/scan_for_new_device.dart
+++ b/lib/Frontend/Widgets/scan_for_new_device.dart
@@ -10,7 +10,7 @@ import '../../Backend/Bluetooth/BluetoothManager.dart';
import '../intnDefs.dart';
class ScanForNewDevice extends ConsumerStatefulWidget {
- const ScanForNewDevice({super.key});
+ ScanForNewDevice({super.key});
@override
ConsumerState createState() => _ScanForNewDevice();
@@ -21,6 +21,7 @@ class _ScanForNewDevice extends ConsumerState {
@override
void initState() {
+ devices = {};
super.initState();
}
@@ -54,23 +55,28 @@ class _ScanForNewDevice extends ConsumerState {
title: Text(scanDevicesAutoConnectTitle()),
),
Wrap(
- children: devices.values
- .map(
- (e) => ListTile(
- title: Text(getNameFromBTName(e.name)),
- trailing: Text(e.id),
- onTap: () {
- ref.watch(knownDevicesProvider.notifier).connect(e);
- setState(
- () {
- devices.remove(e.id);
+ children: [
+ ListView(
+ shrinkWrap: true,
+ children: devices.values
+ .map(
+ (e) => ListTile(
+ title: Text(getNameFromBTName(e.name)),
+ trailing: Text(e.id),
+ onTap: () {
+ ref.watch(knownDevicesProvider.notifier).connect(e);
+ setState(
+ () {
+ devices.remove(e.id);
+ },
+ );
+ Navigator.pop(context);
},
- );
- //Navigator.pop(context);
- },
- ),
- )
- .toList(),
+ ),
+ )
+ .toList(),
+ ),
+ ],
),
Padding(
padding: const EdgeInsets.only(top: 20),
diff --git a/lib/Frontend/pages/Shell.dart b/lib/Frontend/pages/Shell.dart
index 8ded3f51..90eec177 100644
--- a/lib/Frontend/pages/Shell.dart
+++ b/lib/Frontend/pages/Shell.dart
@@ -4,7 +4,6 @@ import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart';
import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
-import 'package:multi_value_listenable_builder/multi_value_listenable_builder.dart';
import 'package:tail_app/Backend/Bluetooth/BluetoothManager.dart';
import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart';
import 'package:tail_app/Frontend/Widgets/scan_for_new_device.dart';
@@ -12,6 +11,7 @@ import 'package:tail_app/Frontend/Widgets/snack_bar_overlay.dart';
import 'package:upgrader/upgrader.dart';
import '../../Backend/AutoMove.dart';
+import '../../main.dart';
import '../intnDefs.dart';
/// Flutter code sample for [NavigationDrawer].
@@ -163,24 +163,30 @@ class _NavigationDrawerExampleState extends ConsumerState 1 ? 200 : 100,
+ width: ref.watch(knownDevicesProvider).values.length > 1 ? 100 : 200,
child: Center(
child: Text(
- "Scan For New Devices",
+ "Scan For New Gear",
textAlign: TextAlign.center,
),
),
),
),
onTap: () {
- showDialog(
+ plausible.event(page: "Scan For New Gear");
+ showModalBottomSheet(
context: context,
- useRootNavigator: false,
+ showDragHandle: true,
+ isScrollControlled: true,
+ enableDrag: true,
+ isDismissible: true,
builder: (BuildContext context) {
- return SimpleDialog(
- title: Text("Scan For New Devices"),
+ return Wrap(
children: [
- const ScanForNewDevice(),
+ ListTile(
+ title: Text("Scan For New Gear"),
+ ),
+ ScanForNewDevice(),
Center(
child: TextButton(
onPressed: () {
@@ -207,188 +213,214 @@ class _NavigationDrawerExampleState extends ConsumerState(
- multiSelectionEnabled: true,
- selected: e.baseStoredDevice.selectedAutoCategories.toSet(),
- onSelectionChanged: (Set value) {
- setState(() {
- e.baseStoredDevice.selectedAutoCategories = value.toList();
- });
- ref.read(knownDevicesProvider.notifier).store();
- ChangeAutoMove(e);
- },
- segments: AutoActionCategory.values.map>(
- (AutoActionCategory value) {
- return ButtonSegment(
- value: value,
- label: Text(value.friendly),
- );
- },
- ).toList(),
- ),
- ),
- ListTile(
- title: Text(manageDevicesAutoMovePauseTitle()),
- subtitle: RangeSlider(
- labels: RangeLabels(manageDevicesAutoMovePauseSliderLabel(e.baseStoredDevice.autoMoveMinPause.round()), manageDevicesAutoMovePauseSliderLabel(e.baseStoredDevice.autoMoveMaxPause.round())),
- min: 15,
- max: 240,
- values: RangeValues(e.baseStoredDevice.autoMoveMinPause, e.baseStoredDevice.autoMoveMaxPause),
- onChanged: (RangeValues value) {
- setState(() {
- e.baseStoredDevice.autoMoveMinPause = value.start;
- e.baseStoredDevice.autoMoveMaxPause = value.end;
- });
- ref.read(knownDevicesProvider.notifier).store();
- },
- onChangeEnd: (values) {
- ChangeAutoMove(e);
- },
- )),
- ListTile(
- title: Text(manageDevicesAutoMoveNoPhoneTitle()),
- subtitle: Slider(
- value: e.baseStoredDevice.noPhoneDelayTime,
- min: 1,
- max: 60,
- onChanged: (double value) {
- setState(() {
- e.baseStoredDevice.noPhoneDelayTime = value;
- });
- ref.read(knownDevicesProvider.notifier).store();
- },
- label: manageDevicesAutoMoveNoPhoneSliderLabel(e.baseStoredDevice.noPhoneDelayTime.round()),
- ),
- ),
- ButtonBar(
- alignment: MainAxisAlignment.end,
- children: [
- TextButton(
- onPressed: () {
- setState(() {
- e.connectionStateStreamSubscription = null;
- });
- },
- child: Text(manageDevicesDisconnect()),
+ ),
),
- TextButton(
- onPressed: () {
- setState(() {
- e.connectionStateStreamSubscription = null;
- });
- ref.watch(knownDevicesProvider.notifier).remove(e.baseStoredDevice.btMACAddress);
- },
- child: Text(manageDevicesForget()),
+ ListTile(
+ title: Text(manageDevicesAutoMoveGroupsTitle()),
+ subtitle: SegmentedButton(
+ multiSelectionEnabled: true,
+ selected: e.baseStoredDevice.selectedAutoCategories.toSet(),
+ onSelectionChanged: (Set value) {
+ setState(() {
+ e.baseStoredDevice.selectedAutoCategories = value.toList();
+ });
+ ref.read(knownDevicesProvider.notifier).store();
+ ChangeAutoMove(e);
+ },
+ segments: AutoActionCategory.values.map>(
+ (AutoActionCategory value) {
+ return ButtonSegment(
+ value: value,
+ label: Text(value.friendly),
+ );
+ },
+ ).toList(),
+ ),
+ ),
+ ListTile(
+ title: Text(manageDevicesAutoMovePauseTitle()),
+ subtitle: RangeSlider(
+ labels: RangeLabels(manageDevicesAutoMovePauseSliderLabel(e.baseStoredDevice.autoMoveMinPause.round()), manageDevicesAutoMovePauseSliderLabel(e.baseStoredDevice.autoMoveMaxPause.round())),
+ min: 15,
+ max: 240,
+ values: RangeValues(e.baseStoredDevice.autoMoveMinPause, e.baseStoredDevice.autoMoveMaxPause),
+ onChanged: (RangeValues value) {
+ setState(() {
+ e.baseStoredDevice.autoMoveMinPause = value.start;
+ e.baseStoredDevice.autoMoveMaxPause = value.end;
+ });
+ ref.read(knownDevicesProvider.notifier).store();
+ },
+ onChangeEnd: (values) {
+ ChangeAutoMove(e);
+ },
+ ),
+ ),
+ ListTile(
+ title: Text(manageDevicesAutoMoveNoPhoneTitle()),
+ subtitle: Slider(
+ value: e.baseStoredDevice.noPhoneDelayTime,
+ min: 1,
+ max: 60,
+ onChanged: (double value) {
+ setState(() {
+ e.baseStoredDevice.noPhoneDelayTime = value;
+ });
+ ref.read(knownDevicesProvider.notifier).store();
+ },
+ label: manageDevicesAutoMoveNoPhoneSliderLabel(e.baseStoredDevice.noPhoneDelayTime.round()),
+ ),
+ ),
+ ButtonBar(
+ alignment: MainAxisAlignment.end,
+ children: [
+ value == DeviceConnectionState.connected
+ ? TextButton(
+ onPressed: () {
+ setState(() {
+ e.connectionStateStreamSubscription?.cancel();
+ });
+ },
+ child: Text(manageDevicesDisconnect()),
+ )
+ : Container(),
+ TextButton(
+ onPressed: () {
+ setState(() {
+ e.connectionStateStreamSubscription = null;
+ });
+ ref.watch(knownDevicesProvider.notifier).remove(e.baseStoredDevice.btMACAddress);
+ },
+ child: Text(manageDevicesForget()),
+ )
+ ],
)
],
- )
- ],
+ );
+ },
);
},
);
- });
- },
- child: Padding(
- padding: const EdgeInsets.all(8.0),
- child: SizedBox(
- height: 50,
- width: 100,
- child: Stack(
- children: [
- Text(e.baseStoredDevice.name),
- Align(
- alignment: Alignment.bottomLeft,
- child: MultiValueListenableBuilder(
- builder: (BuildContext context, List values, Widget? child) {
- if (e.deviceConnectionState.value == DeviceConnectionState.connected) {
- return getBattery(e.battery.value);
- } else {
- return Text(e.deviceConnectionState.value.name);
- }
- },
- valueListenables: [e.battery, e.deviceConnectionState],
+ },
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: SizedBox(
+ height: 50,
+ width: 100,
+ child: Stack(
+ children: [
+ Text(e.baseStoredDevice.name),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: value == DeviceConnectionState.connected
+ ? [
+ ValueListenableBuilder(
+ valueListenable: e.battery,
+ builder: (BuildContext context, value, Widget? child) {
+ return getBattery(e.battery.value);
+ },
+ ),
+ ValueListenableBuilder(
+ valueListenable: e.batteryCharging,
+ builder: (BuildContext context, value, Widget? child) {
+ if (e.deviceConnectionState.value == DeviceConnectionState.connected && e.batteryCharging.value) {
+ return const Icon(Icons.power);
+ } else {
+ return Container();
+ }
+ },
+ ),
+ ValueListenableBuilder(
+ valueListenable: e.rssi,
+ builder: (BuildContext context, value, Widget? child) {
+ return getSignal(e.rssi.value);
+ },
+ ),
+ ]
+ : [const Icon(Icons.bluetooth_disabled)],
+ ),
+ )
+ ],
),
),
- Align(
- alignment: Alignment.bottomRight,
- child: MultiValueListenableBuilder(
- builder: (BuildContext context, List values, Widget? child) {
- if (e.deviceConnectionState.value == DeviceConnectionState.connected) {
- return getSignal(e.rssi.value);
- }
- return Container();
- },
- valueListenables: [e.rssi, e.deviceConnectionState],
- ),
- )
- ],
+ ),
),
),
- ),
- ),
+ );
+ },
);
}
}
diff --git a/lib/Frontend/pages/ota_update.dart b/lib/Frontend/pages/ota_update.dart
new file mode 100644
index 00000000..bac14ac8
--- /dev/null
+++ b/lib/Frontend/pages/ota_update.dart
@@ -0,0 +1,99 @@
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+import 'package:dio/dio.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:tail_app/Backend/Bluetooth/BluetoothManager.dart';
+import 'package:tail_app/Backend/Definitions/Device/BaseDeviceDefinition.dart';
+
+import '../../Backend/FirmwareUpdate.dart';
+
+class OtaUpdate extends ConsumerStatefulWidget {
+ OtaUpdate({super.key, required this.device});
+
+ String device;
+
+ @override
+ _OtaUpdateState createState() => _OtaUpdateState();
+}
+
+class _OtaUpdateState extends ConsumerState {
+ double downloadProgress = 0;
+ double uploadProgress = 0;
+ FWInfo? updateURL;
+ Dio dio = Dio();
+ List? firmwareFile;
+
+ @override
+ Widget build(BuildContext context) {
+ updateURL ??= ref.read(knownDevicesProvider)[widget.device]?.fwInfo.value;
+ downloadFirmware();
+ if (downloadProgress == 1) {
+ uploadFirmware();
+ }
+ return Scaffold(
+ appBar: AppBar(title: Text("Update in progress")),
+ body: Center(
+ child: Column(
+ children: [
+ Text("Updating gear"),
+ ListTile(
+ title: Text("Downloading"),
+ leading: const Icon(Icons.download),
+ subtitle: LinearProgressIndicator(value: downloadProgress),
+ ),
+ ListTile(
+ title: Text("Uploading"),
+ leading: const Icon(Icons.upload),
+ subtitle: LinearProgressIndicator(value: uploadProgress),
+ )
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future downloadFirmware() async {
+ final Response> rs = await Dio().get>(updateURL!.url, options: Options(responseType: ResponseType.bytes), onReceiveProgress: (current, total) {
+ setState(() {
+ downloadProgress = current / total;
+ });
+ });
+ if (rs.statusCode == 200) {
+ downloadProgress = 1;
+ Digest digest = md5.convert(rs.data!);
+ if (digest.toString() == updateURL!.md5sum) {
+ firmwareFile = rs.data;
+ }
+ }
+ }
+
+ Future uploadFirmware() async {
+ BaseStatefulDevice? baseStatefulDevice = ref.read(knownDevicesProvider)[widget.device];
+ if (firmwareFile != null && baseStatefulDevice != null) {
+ int mtu = await ref.read(reactiveBLEProvider).requestMtu(deviceId: baseStatefulDevice.baseStoredDevice.btMACAddress, mtu: 512) - 10;
+ int total = firmwareFile!.length;
+ int current = 0;
+ while (uploadProgress < 1) {
+ baseStatefulDevice.deviceState.value = DeviceState.busy; // hold the command queue
+ int nextEnd = current + mtu;
+ if (nextEnd > total) {
+ nextEnd = total;
+ }
+ List chunk = firmwareFile!.sublist(current, nextEnd);
+ if (current == 0) {
+ List beginOTA = const Utf8Encoder().convert("OTA ");
+ beginOTA.addAll(chunk);
+ chunk = beginOTA;
+ }
+ await ref.read(reactiveBLEProvider).writeCharacteristicWithResponse(baseStatefulDevice.txCharacteristic, value: chunk);
+ current = current + chunk.length;
+ setState(() {
+ uploadProgress = current / total;
+ });
+ }
+ baseStatefulDevice.deviceState.value = DeviceState.standby; // hold the command queue
+ }
+ }
+}
diff --git a/lib/main.dart b/lib/main.dart
index b891fc74..d621a559 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:developer';
+import 'package:dio/dio.dart';
import 'package:feedback_sentry/feedback_sentry.dart';
import 'package:fk_user_agent/fk_user_agent.dart';
import 'package:flex_color_scheme/flex_color_scheme.dart';
@@ -13,6 +14,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging_flutter/logging_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:plausible_analytics/plausible_analytics.dart';
+import 'package:sentry_dio/sentry_dio.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_hive/sentry_hive.dart';
import 'package:sentry_logging/sentry_logging.dart';
@@ -92,8 +94,8 @@ Future main() async {
(options) {
options.dsn = 'https://1c6815c83f0644db8d569f0ba454f035@glitchtip.codel1417.xyz/2';
options.addIntegration(LoggingIntegration());
- options.attachScreenshot = true;
- options.attachViewHierarchy = true;
+ options.attachScreenshot = true; //not supported on GlitchTip
+ options.attachViewHierarchy = true; //not supported on GlitchTip
options.tracesSampleRate = 1.0;
options.profilesSampleRate = 1.0;
options.attachThreads = true;
@@ -124,6 +126,14 @@ Future main() async {
);
}
+void initDio() {
+ final dio = Dio();
+
+ /// This *must* be the last initialization step of the Dio setup, otherwise
+ /// your configuration of Dio might overwrite the Sentry configuration.
+ dio.addSentry();
+}
+
class TailApp extends ConsumerWidget {
const TailApp({super.key});
diff --git a/pubspec.lock b/pubspec.lock
index 1ba7acc6..8e9c8c7f 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: built_value
- sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
+ sha256: fedde275e0a6b798c3296963c5cd224e3e1b55d0e478d5b7e65e6b540f363a0e
url: "https://pub.dev"
source: hosted
- version: "8.9.0"
+ version: "8.9.1"
characters:
dependency: transitive
description:
@@ -218,7 +218,7 @@ packages:
source: hosted
version: "3.0.1"
crypto:
- dependency: transitive
+ dependency: "direct main"
description:
name: crypto
sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab
@@ -281,6 +281,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.0"
+ dio:
+ dependency: "direct main"
+ description:
+ name: dio
+ sha256: "49af28382aefc53562459104f64d16b9dfd1e8ef68c862d5af436cc8356ce5a8"
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.4.1"
double_back_to_close_app:
dependency: "direct main"
description:
@@ -427,6 +435,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.10"
+ flutter_nearby_connections:
+ dependency: "direct main"
+ description:
+ name: flutter_nearby_connections
+ sha256: f2003fde3df5d99ce15693b1603de168f66a2e7c50b84786191236cc32e3afdc
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
flutter_reactive_ble:
dependency: "direct main"
description:
@@ -629,6 +645,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.8.1"
+ json_serializable:
+ dependency: "direct main"
+ description:
+ name: json_serializable
+ sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.7.1"
lints:
dependency: transitive
description:
@@ -1053,6 +1077,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.7.1"
+ sentry_dio:
+ dependency: "direct main"
+ description:
+ name: sentry_dio
+ sha256: "3812f44fb6657cb04806cbfbd1dd03d00fb6aceb06bf5dd236ecff56b754a1cf"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.16.1"
sentry_flutter:
dependency: "direct main"
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index 671491e3..d5e52aca 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -27,6 +27,7 @@ dependencies:
sentry_flutter: ^7.16.1
sentry_logging: ^7.16.1
sentry_hive: ^7.16.1
+ sentry_dio: ^7.16.1
vector_math: ^2.1.4
collection: ^1.18.0
flutter_foreground_service: ^0.4.1
@@ -47,6 +48,10 @@ dependencies:
uuid: ^4.3.3
plausible_analytics: ^0.3.0
fk_user_agent: ^2.1.0
+ flutter_nearby_connections: ^1.1.2
+ dio: ^5.4.1
+ json_serializable: ^6.7.1
+ crypto: ^3.0.3
dev_dependencies:
build_runner:
flutter_test:
@@ -57,7 +62,7 @@ dev_dependencies:
riverpod_generator:
flutter_native_splash: ^2.3.10
riverpod_annotation: ^2.3.4
- sentry_dart_plugin: ^1.7.0
+ sentry_dart_plugin: ^1.7.1
intl_translation:
hive_generator: ^2.0.1
flutter: