mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-18 00:17:40 +01:00
Create base classes MdnsEmulator and BluetoothEmulator with shared logic
Co-authored-by: jonasbark <1151304+jonasbark@users.noreply.github.com>
This commit is contained in:
174
lib/bluetooth/devices/bluetooth_emulator.dart
Normal file
174
lib/bluetooth/devices/bluetooth_emulator.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
/// Base class for Bluetooth Low Energy (BLE) peripheral emulators.
|
||||
/// Provides common functionality for peripheral management, service advertising,
|
||||
/// and connection state handling.
|
||||
abstract class BluetoothEmulator extends TrainerConnection {
|
||||
final _peripheralManager = PeripheralManager();
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
Central? _central;
|
||||
|
||||
BluetoothEmulator({
|
||||
required super.title,
|
||||
required super.supportedActions,
|
||||
});
|
||||
|
||||
/// Gets the peripheral manager instance.
|
||||
@protected
|
||||
PeripheralManager get peripheralManager => _peripheralManager;
|
||||
|
||||
/// Gets whether the emulator is currently loading/initializing.
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
/// Sets the loading state.
|
||||
@protected
|
||||
set isLoading(bool value) => _isLoading = value;
|
||||
|
||||
/// Gets whether services have been added to the peripheral manager.
|
||||
@protected
|
||||
bool get isServiceAdded => _isServiceAdded;
|
||||
|
||||
/// Sets whether services have been added.
|
||||
@protected
|
||||
set isServiceAdded(bool value) => _isServiceAdded = value;
|
||||
|
||||
/// Gets whether event subscriptions are active.
|
||||
@protected
|
||||
bool get isSubscribedToEvents => _isSubscribedToEvents;
|
||||
|
||||
/// Sets whether event subscriptions are active.
|
||||
@protected
|
||||
set isSubscribedToEvents(bool value) => _isSubscribedToEvents = value;
|
||||
|
||||
/// Gets the current connected central device.
|
||||
@protected
|
||||
Central? get central => _central;
|
||||
|
||||
/// Sets the connected central device.
|
||||
@protected
|
||||
set central(Central? value) => _central = value;
|
||||
|
||||
/// Subscribes to peripheral manager state changes.
|
||||
@protected
|
||||
void subscribeToStateChanges() {
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
if (kDebugMode) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Subscribes to connection state changes on Android.
|
||||
/// Handles connection and disconnection events.
|
||||
@protected
|
||||
void subscribeToConnectionStateChanges(VoidCallback? onUpdate) {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
if (kDebugMode) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
}
|
||||
if (state.state == ConnectionState.connected) {
|
||||
// Override in subclass if needed
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
handleDisconnection();
|
||||
onUpdate?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles disconnection events. Can be overridden by subclasses.
|
||||
@protected
|
||||
void handleDisconnection() {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
}
|
||||
|
||||
/// Requests Bluetooth advertise permission on Android.
|
||||
/// Returns true if permission is granted, false otherwise.
|
||||
@protected
|
||||
Future<bool> requestBluetoothAdvertisePermission() async {
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
if (kDebugMode) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Waits for the peripheral manager to be powered on.
|
||||
/// Returns true if powered on, false if cancelled (e.g., by user action).
|
||||
@protected
|
||||
Future<bool> waitForPoweredOn(bool Function() shouldContinue) async {
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn && shouldContinue()) {
|
||||
if (kDebugMode) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
}
|
||||
return _peripheralManager.state == BluetoothLowEnergyState.poweredOn;
|
||||
}
|
||||
|
||||
/// Adds a GATT service to the peripheral manager.
|
||||
@protected
|
||||
Future<void> addService(GATTService service) async {
|
||||
await _peripheralManager.addService(service);
|
||||
}
|
||||
|
||||
/// Starts advertising with the given advertisement configuration.
|
||||
@protected
|
||||
Future<void> startAdvertising(Advertisement advertisement) async {
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
}
|
||||
|
||||
/// Stops advertising and resets state.
|
||||
@protected
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
|
||||
/// Notifies a characteristic with the given value to the connected central.
|
||||
@protected
|
||||
Future<void> notifyCharacteristic(
|
||||
Central central,
|
||||
GATTCharacteristic characteristic, {
|
||||
required Uint8List value,
|
||||
}) async {
|
||||
await _peripheralManager.notifyCharacteristic(central, characteristic, value: value);
|
||||
}
|
||||
|
||||
/// Cleans up resources by stopping advertising and removing services.
|
||||
@protected
|
||||
void cleanup() {
|
||||
_peripheralManager.stopAdvertising();
|
||||
_peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
_isSubscribedToEvents = false;
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
isStarted.value = false;
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
153
lib/bluetooth/devices/mdns_emulator.dart
Normal file
153
lib/bluetooth/devices/mdns_emulator.dart
Normal file
@@ -0,0 +1,153 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nsd/nsd.dart';
|
||||
|
||||
/// Base class for mDNS-based emulators that advertise services over the network.
|
||||
/// Provides common functionality for TCP server management, mDNS registration,
|
||||
/// and network interface discovery.
|
||||
abstract class MdnsEmulator extends TrainerConnection {
|
||||
ServerSocket? _tcpServer;
|
||||
Registration? _mdnsRegistration;
|
||||
Socket? _socket;
|
||||
|
||||
MdnsEmulator({
|
||||
required super.title,
|
||||
required super.supportedActions,
|
||||
});
|
||||
|
||||
/// Gets the TCP server instance, if available.
|
||||
@protected
|
||||
ServerSocket? get tcpServer => _tcpServer;
|
||||
|
||||
/// Gets the current client socket connection, if available.
|
||||
@protected
|
||||
Socket? get socket => _socket;
|
||||
|
||||
/// Sets the client socket connection.
|
||||
@protected
|
||||
set socket(Socket? value) => _socket = value;
|
||||
|
||||
/// Gets the mDNS registration, if available.
|
||||
@protected
|
||||
Registration? get mdnsRegistration => _mdnsRegistration;
|
||||
|
||||
/// Finds and returns a local IPv4 address from available network interfaces.
|
||||
/// Returns null if no suitable address is found.
|
||||
@protected
|
||||
Future<InternetAddress?> findLocalIP() async {
|
||||
final interfaces = await NetworkInterface.list();
|
||||
InternetAddress? localIP;
|
||||
|
||||
for (final interface in interfaces) {
|
||||
for (final addr in interface.addresses) {
|
||||
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
|
||||
localIP = addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (localIP != null) break;
|
||||
}
|
||||
|
||||
return localIP;
|
||||
}
|
||||
|
||||
/// Creates a TCP server on the specified port.
|
||||
/// The server listens on all IPv6 addresses (with v6Only: false to also accept IPv4).
|
||||
@protected
|
||||
Future<ServerSocket> createTcpServer(int port) async {
|
||||
try {
|
||||
_tcpServer = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv6,
|
||||
port,
|
||||
shared: true,
|
||||
v6Only: false,
|
||||
);
|
||||
if (kDebugMode) {
|
||||
print('TCP Server started on port ${_tcpServer!.port}');
|
||||
}
|
||||
return _tcpServer!;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to start server: $e');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers an mDNS service with the given configuration.
|
||||
@protected
|
||||
Future<Registration> registerMdnsService(Service service) async {
|
||||
if (kDebugMode) {
|
||||
enableLogging(LogTopic.calls);
|
||||
enableLogging(LogTopic.errors);
|
||||
}
|
||||
disableServiceTypeValidation(true);
|
||||
|
||||
_mdnsRegistration = await register(service);
|
||||
if (kDebugMode) {
|
||||
print('mDNS service registered: ${service.name}');
|
||||
}
|
||||
return _mdnsRegistration!;
|
||||
}
|
||||
|
||||
/// Unregisters the mDNS service if one is registered.
|
||||
@protected
|
||||
void unregisterMdnsService() {
|
||||
if (_mdnsRegistration != null) {
|
||||
unregister(_mdnsRegistration!);
|
||||
_mdnsRegistration = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the TCP server and client socket.
|
||||
@protected
|
||||
void closeTcpServer() {
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
_tcpServer?.close();
|
||||
_tcpServer = null;
|
||||
}
|
||||
|
||||
/// Writes data to the client socket.
|
||||
@protected
|
||||
void writeToSocket(Socket socket, List<int> data) {
|
||||
if (kDebugMode) {
|
||||
print('Sending response: ${bytesToHex(data)}');
|
||||
}
|
||||
socket.add(data);
|
||||
}
|
||||
|
||||
/// Stops the emulator by closing connections and unregistering services.
|
||||
void stop() {
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
closeTcpServer();
|
||||
unregisterMdnsService();
|
||||
if (kDebugMode) {
|
||||
print('Stopped ${runtimeType}');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts a list of bytes to a hexadecimal string representation.
|
||||
String bytesToHex(List<int> bytes, {bool spaced = false}) {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(spaced ? ' ' : '');
|
||||
}
|
||||
|
||||
/// Converts a list of bytes to a readable hexadecimal string with spaces.
|
||||
String bytesToReadableHex(List<int> bytes) {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
}
|
||||
|
||||
/// Converts a hexadecimal string to a list of bytes.
|
||||
List<int> hexToBytes(String hex) {
|
||||
final bytes = <int>[];
|
||||
for (var i = 0; i < hex.length; i += 2) {
|
||||
final byte = hex.substring(i, i + 2);
|
||||
bytes.add(int.parse(byte, radix: 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/bluetooth_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
@@ -15,12 +15,8 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../../messages/notification.dart' show AlertNotification;
|
||||
|
||||
class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
class OpenBikeControlBluetoothEmulator extends BluetoothEmulator {
|
||||
final ValueNotifier<AppInfo?> connectedApp = ValueNotifier<AppInfo?>(null);
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
Central? _central;
|
||||
|
||||
late GATTCharacteristic _buttonCharacteristic;
|
||||
|
||||
@@ -35,13 +31,13 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
Future<void> startServer() async {
|
||||
isStarted.value = true;
|
||||
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
subscribeToStateChanges();
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
peripheralManager.connectionStateChanged.forEach((state) {
|
||||
if (kDebugMode) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
}
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
if (connectedApp.value != null) {
|
||||
@@ -51,14 +47,13 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
}
|
||||
isConnected.value = false;
|
||||
connectedApp.value = null;
|
||||
_central = null;
|
||||
central = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn && core.settings.getObpBleEnabled()) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
if (!await waitForPoweredOn(() => core.settings.getObpBleEnabled())) {
|
||||
return;
|
||||
}
|
||||
|
||||
_buttonCharacteristic = GATTCharacteristic.mutable(
|
||||
@@ -70,12 +65,12 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
permissions: [],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
if (!isServiceAdded) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
_peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
if (!isSubscribedToEvents) {
|
||||
isSubscribedToEvents = true;
|
||||
peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
print('Read request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toUpperCase()) {
|
||||
@@ -85,20 +80,20 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
|
||||
final request = eventArgs.request;
|
||||
final trimmedValue = Uint8List.fromList([]);
|
||||
await _peripheralManager.respondReadRequestWithValue(
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
request,
|
||||
value: trimmedValue,
|
||||
);
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
_central = char.central;
|
||||
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
central = char.central;
|
||||
print(
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}',
|
||||
);
|
||||
});
|
||||
_peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
final characteristic = eventArgs.characteristic;
|
||||
final request = eventArgs.request;
|
||||
final value = request.value;
|
||||
@@ -125,13 +120,13 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
print('Unhandled write request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
}
|
||||
|
||||
await _peripheralManager.respondWriteRequest(request);
|
||||
await peripheralManager.respondWriteRequest(request);
|
||||
});
|
||||
}
|
||||
|
||||
if (!Platform.isWindows) {
|
||||
// Device Information
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180A'),
|
||||
isPrimary: true,
|
||||
@@ -162,7 +157,7 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
);
|
||||
}
|
||||
// Battery Service
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
@@ -184,7 +179,7 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
);
|
||||
|
||||
// Unknown Service
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString(OpenBikeControlConstants.SERVICE_UUID),
|
||||
isPrimary: true,
|
||||
@@ -206,7 +201,7 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
@@ -215,16 +210,14 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
);
|
||||
print('Starting advertising with OpenBikeControl service...');
|
||||
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
await startAdvertising(advertisement);
|
||||
}
|
||||
|
||||
Future<void> stopServer() async {
|
||||
if (kDebugMode) {
|
||||
print('Stopping OpenBikeControl BLE server...');
|
||||
}
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
await stopAdvertising();
|
||||
connectedApp.value = null;
|
||||
}
|
||||
|
||||
@@ -238,7 +231,7 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
|
||||
if (inGameAction == null) {
|
||||
return Error('Invalid in-game action for key pair: $keyPair');
|
||||
} else if (_central == null) {
|
||||
} else if (central == null) {
|
||||
return Error('No central connected');
|
||||
} else if (connectedApp.value == null) {
|
||||
return Error('No app info received from central');
|
||||
@@ -250,16 +243,16 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
|
||||
final responseDataDown = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, 1)).toList(),
|
||||
);
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _buttonCharacteristic, value: responseDataDown);
|
||||
await notifyCharacteristic(central!, _buttonCharacteristic, value: responseDataDown);
|
||||
final responseDataUp = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, 0)).toList(),
|
||||
);
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _buttonCharacteristic, value: responseDataUp);
|
||||
await notifyCharacteristic(central!, _buttonCharacteristic, value: responseDataUp);
|
||||
} else {
|
||||
final responseData = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, isKeyDown ? 1 : 0)).toList(),
|
||||
);
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _buttonCharacteristic, value: responseData);
|
||||
await notifyCharacteristic(central!, _buttonCharacteristic, value: responseData);
|
||||
}
|
||||
|
||||
return Success('Buttons ${inGameAction.title} sent');
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pb.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
@@ -14,16 +12,11 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nsd/nsd.dart';
|
||||
|
||||
class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
ServerSocket? _server;
|
||||
Registration? _mdnsRegistration;
|
||||
|
||||
class OpenBikeControlMdnsEmulator extends MdnsEmulator {
|
||||
static const String connectionTitle = 'OpenBikeControl mDNS Emulator';
|
||||
|
||||
final ValueNotifier<AppInfo?> connectedApp = ValueNotifier(null);
|
||||
|
||||
Socket? _socket;
|
||||
|
||||
OpenBikeControlMdnsEmulator()
|
||||
: super(
|
||||
title: connectionTitle,
|
||||
@@ -35,18 +28,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
isStarted.value = true;
|
||||
|
||||
// Get local IP
|
||||
final interfaces = await NetworkInterface.list();
|
||||
InternetAddress? localIP;
|
||||
|
||||
for (final interface in interfaces) {
|
||||
for (final addr in interface.addresses) {
|
||||
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
|
||||
localIP = addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (localIP != null) break;
|
||||
}
|
||||
final localIP = await findLocalIP();
|
||||
|
||||
if (localIP == null) {
|
||||
throw 'Could not find network interface';
|
||||
@@ -54,15 +36,9 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
|
||||
await _createTcpServer();
|
||||
|
||||
if (kDebugMode) {
|
||||
enableLogging(LogTopic.calls);
|
||||
enableLogging(LogTopic.errors);
|
||||
}
|
||||
disableServiceTypeValidation(true);
|
||||
|
||||
try {
|
||||
// Create service
|
||||
_mdnsRegistration = await register(
|
||||
await registerMdnsService(
|
||||
Service(
|
||||
name: 'BikeControl',
|
||||
type: '_openbikecontrol._tcp',
|
||||
@@ -79,7 +55,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
},
|
||||
),
|
||||
);
|
||||
print('Service: ${_mdnsRegistration!.id} at ${localIP.address}:$_mdnsRegistration');
|
||||
print('Service: ${mdnsRegistration!.id} at ${localIP.address}:$mdnsRegistration');
|
||||
print('Server started - advertising service!');
|
||||
} catch (e, s) {
|
||||
core.connection.signalNotification(AlertNotification(LogLevel.LOGLEVEL_ERROR, 'Failed to start mDNS server: $e'));
|
||||
@@ -91,37 +67,17 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
if (kDebugMode) {
|
||||
print('Stopping OpenBikeControl mDNS server...');
|
||||
}
|
||||
if (_mdnsRegistration != null) {
|
||||
unregister(_mdnsRegistration!);
|
||||
_mdnsRegistration = null;
|
||||
}
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
stop();
|
||||
connectedApp.value = null;
|
||||
_socket?.destroy();
|
||||
_socket = null;
|
||||
}
|
||||
|
||||
Future<void> _createTcpServer() async {
|
||||
try {
|
||||
_server = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv6,
|
||||
36867,
|
||||
shared: true,
|
||||
v6Only: false,
|
||||
);
|
||||
} catch (e) {
|
||||
core.connection.signalNotification(AlertNotification(LogLevel.LOGLEVEL_ERROR, 'Failed to start server: $e'));
|
||||
rethrow;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('Server started on port ${_server!.port}');
|
||||
}
|
||||
await createTcpServer(36867);
|
||||
|
||||
// Accept connection
|
||||
_server!.listen(
|
||||
tcpServer!.listen(
|
||||
(Socket socket) {
|
||||
_socket = socket;
|
||||
this.socket = socket;
|
||||
|
||||
if (kDebugMode) {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
@@ -155,7 +111,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
);
|
||||
isConnected.value = false;
|
||||
connectedApp.value = null;
|
||||
_socket = null;
|
||||
this.socket = null;
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -172,7 +128,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
|
||||
if (inGameAction == null) {
|
||||
return Error('Invalid in-game action for key pair: $keyPair');
|
||||
} else if (_socket == null) {
|
||||
} else if (socket == null) {
|
||||
print('No client connected, cannot send button press');
|
||||
return Error('No client connected');
|
||||
} else if (connectedApp.value == null) {
|
||||
@@ -185,23 +141,18 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
final responseDataDown = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, 1)).toList(),
|
||||
);
|
||||
_write(_socket!, responseDataDown);
|
||||
writeToSocket(socket!, responseDataDown);
|
||||
final responseDataUp = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, 0)).toList(),
|
||||
);
|
||||
_write(_socket!, responseDataUp);
|
||||
writeToSocket(socket!, responseDataUp);
|
||||
} else {
|
||||
final responseData = OpenBikeProtocolParser.encodeButtonState(
|
||||
mappedButtons.map((b) => ButtonState(b, isKeyDown ? 1 : 0)).toList(),
|
||||
);
|
||||
_write(_socket!, responseData);
|
||||
writeToSocket(socket!, responseData);
|
||||
}
|
||||
|
||||
return Success('Sent ${inGameAction.title} button press');
|
||||
}
|
||||
|
||||
void _write(Socket socket, List<int> responseData) {
|
||||
debugPrint('Sending response: ${bytesToHex(responseData)}');
|
||||
socket.add(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pbenum.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zwift.pb.dart' show RideKeyPadStatus;
|
||||
@@ -15,13 +15,9 @@ import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nsd/nsd.dart';
|
||||
|
||||
class FtmsMdnsEmulator extends TrainerConnection {
|
||||
ServerSocket? _tcpServer;
|
||||
Registration? _mdnsRegistration;
|
||||
|
||||
class FtmsMdnsEmulator extends MdnsEmulator {
|
||||
static const String connectionTitle = 'Zwift Network Emulator';
|
||||
|
||||
Socket? _socket;
|
||||
var lastMessageId = 0;
|
||||
|
||||
FtmsMdnsEmulator()
|
||||
@@ -46,18 +42,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
print('Starting mDNS server...');
|
||||
|
||||
// Get local IP
|
||||
final interfaces = await NetworkInterface.list();
|
||||
InternetAddress? localIP;
|
||||
|
||||
for (final interface in interfaces) {
|
||||
for (final addr in interface.addresses) {
|
||||
if (addr.type == InternetAddressType.IPv4 && !addr.isLoopback) {
|
||||
localIP = addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (localIP != null) break;
|
||||
}
|
||||
final localIP = await findLocalIP();
|
||||
|
||||
if (localIP == null) {
|
||||
throw 'Could not find network interface';
|
||||
@@ -65,13 +50,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
|
||||
await _createTcpServer();
|
||||
|
||||
if (kDebugMode) {
|
||||
enableLogging(LogTopic.calls);
|
||||
enableLogging(LogTopic.errors);
|
||||
}
|
||||
disableServiceTypeValidation(true);
|
||||
|
||||
_mdnsRegistration = await register(
|
||||
await registerMdnsService(
|
||||
Service(
|
||||
name: 'KICKR BIKE PRO 1337',
|
||||
addresses: [localIP],
|
||||
@@ -87,41 +66,13 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
print('Server started - advertising service!');
|
||||
}
|
||||
|
||||
void stop() {
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_tcpServer?.close();
|
||||
if (_mdnsRegistration != null) {
|
||||
unregister(_mdnsRegistration!);
|
||||
}
|
||||
_tcpServer = null;
|
||||
_mdnsRegistration = null;
|
||||
_socket = null;
|
||||
print('Stopped FtmsMdnsEmulator');
|
||||
}
|
||||
|
||||
Future<void> _createTcpServer() async {
|
||||
try {
|
||||
_tcpServer = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv6,
|
||||
36867,
|
||||
shared: true,
|
||||
v6Only: false,
|
||||
);
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('Failed to start server: $e');
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('Server started on port ${_tcpServer!.port}');
|
||||
}
|
||||
await createTcpServer(36867);
|
||||
|
||||
// Accept connection
|
||||
_tcpServer!.listen(
|
||||
tcpServer!.listen(
|
||||
(Socket socket) {
|
||||
_socket = socket;
|
||||
this.socket = socket;
|
||||
isConnected.value = true;
|
||||
if (kDebugMode) {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
@@ -171,7 +122,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
|
||||
// Expected 0101000000100000fc8200001000800000805f9b34fb
|
||||
// Got 0101000000100000fc8200001000800000805f9b34fb
|
||||
_write(socket, bytes);
|
||||
writeToSocket(socket, bytes);
|
||||
case FtmsMdnsConstants.DC_MESSAGE_DISCOVER_CHARACTERISTICS:
|
||||
final rawUUID = body.takeBytes(16);
|
||||
final serviceUUID = bytesToHex(rawUUID).toUUID();
|
||||
@@ -201,7 +152,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
];
|
||||
|
||||
// OK: 0102010000430000fc8200001000800000805f9b34fb0000000319ca465186e5fa29dcdd09d1020000000219ca465186e5fa29dcdd09d1040000000419ca465186e5fa29dcdd09d104
|
||||
_write(socket, responseData);
|
||||
writeToSocket(socket, responseData);
|
||||
}
|
||||
case FtmsMdnsConstants.DC_MESSAGE_READ_CHARACTERISTIC:
|
||||
final rawUUID = body.takeBytes(16);
|
||||
@@ -220,7 +171,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
...responseBody,
|
||||
];
|
||||
|
||||
_write(socket, responseData);
|
||||
writeToSocket(socket, responseData);
|
||||
case FtmsMdnsConstants.DC_MESSAGE_WRITE_CHARACTERISTIC:
|
||||
final rawUUID = body.takeBytes(16);
|
||||
final characteristicUUID = bytesToHex(rawUUID).toUUID();
|
||||
@@ -239,7 +190,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
...responseBody,
|
||||
];
|
||||
|
||||
_write(socket, responseData);
|
||||
writeToSocket(socket, responseData);
|
||||
|
||||
final response = core.zwiftEmulator.handleWriteRequest(
|
||||
characteristicUUID,
|
||||
@@ -269,7 +220,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
];
|
||||
|
||||
// 0106050000180000000419ca465186e5fa29dcdd09d1526964654f6e0203
|
||||
_write(socket, responseData);
|
||||
writeToSocket(socket, responseData);
|
||||
|
||||
if (response.contentEquals(ZwiftConstants.RIDE_ON)) {
|
||||
_sendKeepAlive();
|
||||
@@ -293,7 +244,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
...responseBody,
|
||||
];
|
||||
|
||||
_write(socket, responseData);
|
||||
writeToSocket(socket, responseData);
|
||||
case FtmsMdnsConstants.DC_MESSAGE_CHARACTERISTIC_NOTIFICATION:
|
||||
print('Hamlo');
|
||||
default:
|
||||
@@ -308,20 +259,13 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
_socket = null;
|
||||
this.socket = null;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _write(Socket socket, List<int> responseData) {
|
||||
if (kDebugMode) {
|
||||
print('Sending response: ${bytesToHex(responseData)}');
|
||||
}
|
||||
socket.add(responseData);
|
||||
}
|
||||
|
||||
int _propertyVal(List<String> properties) {
|
||||
int res = 0;
|
||||
|
||||
@@ -368,7 +312,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
]),
|
||||
);
|
||||
|
||||
_write(_socket!, commandProto);
|
||||
writeToSocket(socket!, commandProto);
|
||||
}
|
||||
|
||||
if (isKeyUp) {
|
||||
@@ -377,7 +321,7 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
Uint8List.fromList([Opcode.CONTROLLER_NOTIFICATION.value, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]),
|
||||
);
|
||||
|
||||
_write(_socket!, zero);
|
||||
writeToSocket(socket!, zero);
|
||||
}
|
||||
if (kDebugMode) {
|
||||
print('Sent action $isKeyUp vs $isKeyDown ${keyPair.inGameAction!.title} to Zwift Emulator');
|
||||
@@ -411,9 +355,9 @@ class FtmsMdnsEmulator extends TrainerConnection {
|
||||
|
||||
Future<void> _sendKeepAlive() async {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
if (_socket != null) {
|
||||
_write(
|
||||
_socket!,
|
||||
if (socket != null) {
|
||||
writeToSocket(
|
||||
socket!,
|
||||
_buildNotify(
|
||||
ZwiftConstants.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID,
|
||||
hexToBytes('B70100002041201C00180004001B4F00B701000020798EC5BDEFCBE4563418269E4926FBE1'),
|
||||
@@ -464,23 +408,6 @@ extension on List<int> {
|
||||
}
|
||||
}
|
||||
|
||||
String bytesToHex(List<int> bytes, {bool spaced = false}) {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(spaced ? ' ' : '');
|
||||
}
|
||||
|
||||
String bytesToReadableHex(List<int> bytes) {
|
||||
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||
}
|
||||
|
||||
List<int> hexToBytes(String hex) {
|
||||
final bytes = <int>[];
|
||||
for (var i = 0; i < hex.length; i += 2) {
|
||||
final byte = hex.substring(i, i + 2);
|
||||
bytes.add(int.parse(byte, radix: 16));
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
class FtmsMdnsConstants {
|
||||
static const DC_RC_REQUEST_COMPLETED_SUCCESSFULLY = 0; // Request completed successfully
|
||||
static const DC_RC_UNKNOWN_MESSAGE_TYPE = 1; // Unknown Message Type
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/ble.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/bluetooth_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/ftms_mdns_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pb.dart';
|
||||
@@ -17,18 +17,10 @@ import 'package:bike_control/utils/requirements/multi.dart';
|
||||
import 'package:bike_control/widgets/title.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
class ZwiftEmulator extends TrainerConnection {
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
class ZwiftEmulator extends BluetoothEmulator {
|
||||
static const String connectionTitle = 'Zwift BLE Emulator';
|
||||
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
Central? _central;
|
||||
GATTCharacteristic? _asyncCharacteristic;
|
||||
GATTCharacteristic? _syncTxCharacteristic;
|
||||
|
||||
@@ -50,51 +42,30 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
);
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await peripheralManager.stopAdvertising();
|
||||
await peripheralManager.removeAllServices();
|
||||
isServiceAdded = false;
|
||||
startAdvertising(() {});
|
||||
}
|
||||
|
||||
Future<void> startAdvertising(VoidCallback onUpdate) async {
|
||||
_isLoading = true;
|
||||
isLoading = true;
|
||||
isStarted.value = true;
|
||||
onUpdate();
|
||||
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
subscribeToStateChanges();
|
||||
subscribeToConnectionStateChanges(onUpdate);
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
onUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
isStarted.value = false;
|
||||
onUpdate();
|
||||
return;
|
||||
}
|
||||
if (!await requestBluetoothAdvertisePermission()) {
|
||||
isStarted.value = false;
|
||||
onUpdate();
|
||||
return;
|
||||
}
|
||||
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn &&
|
||||
core.settings.getZwiftBleEmulatorEnabled()) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (!await waitForPoweredOn(() => core.settings.getZwiftBleEmulatorEnabled())) {
|
||||
if (core.settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
|
||||
_syncTxCharacteristic = GATTCharacteristic.mutable(
|
||||
@@ -118,12 +89,12 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
permissions: [],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
if (!isServiceAdded) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
_peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
if (!isSubscribedToEvents) {
|
||||
isSubscribedToEvents = true;
|
||||
peripheralManager.characteristicReadRequested.forEach((eventArgs) async {
|
||||
print('Read request for characteristic: ${eventArgs.characteristic.uuid}');
|
||||
|
||||
switch (eventArgs.characteristic.uuid.toString().toUpperCase()) {
|
||||
@@ -131,7 +102,7 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
print('Handling read request for SYNC TX characteristic');
|
||||
break;
|
||||
case BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_BATTERY_LEVEL:
|
||||
await _peripheralManager.respondReadRequestWithValue(
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
eventArgs.request,
|
||||
value: Uint8List.fromList([100]),
|
||||
);
|
||||
@@ -142,20 +113,20 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
|
||||
final request = eventArgs.request;
|
||||
final trimmedValue = Uint8List.fromList([]);
|
||||
await _peripheralManager.respondReadRequestWithValue(
|
||||
await peripheralManager.respondReadRequestWithValue(
|
||||
request,
|
||||
value: trimmedValue,
|
||||
);
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
print(
|
||||
'Notify state changed for characteristic: ${char.characteristic.uuid}: ${char.state}',
|
||||
);
|
||||
});
|
||||
_peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
_central = eventArgs.central;
|
||||
peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
|
||||
central = eventArgs.central;
|
||||
isConnected.value = true;
|
||||
|
||||
core.connection.signalNotification(
|
||||
@@ -165,8 +136,8 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
final request = eventArgs.request;
|
||||
final response = handleWriteRequest(eventArgs.characteristic.uuid.toString(), request.value);
|
||||
if (response != null) {
|
||||
await _peripheralManager.notifyCharacteristic(
|
||||
_central!,
|
||||
await notifyCharacteristic(
|
||||
central!,
|
||||
_syncTxCharacteristic!,
|
||||
value: response,
|
||||
);
|
||||
@@ -176,13 +147,13 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
}
|
||||
}
|
||||
|
||||
await _peripheralManager.respondWriteRequest(request);
|
||||
await peripheralManager.respondWriteRequest(request);
|
||||
});
|
||||
}
|
||||
|
||||
if (!Platform.isWindows) {
|
||||
// Device Information
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180A'),
|
||||
isPrimary: true,
|
||||
@@ -213,7 +184,7 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
);
|
||||
}
|
||||
// Battery Service
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
@@ -235,7 +206,7 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
);
|
||||
|
||||
// Unknown Service
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString(ZwiftConstants.ZWIFT_CUSTOM_SERVICE_UUID),
|
||||
isPrimary: true,
|
||||
@@ -276,7 +247,7 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
@@ -294,23 +265,21 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
);
|
||||
print('Starting advertising with Zwift service...');
|
||||
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
_isLoading = false;
|
||||
await startAdvertising(advertisement);
|
||||
isLoading = false;
|
||||
onUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_isLoading = false;
|
||||
await super.stopAdvertising();
|
||||
}
|
||||
|
||||
Future<void> _sendKeepAlive() async {
|
||||
await Future.delayed(const Duration(seconds: 5));
|
||||
if (isConnected.value && _central != null) {
|
||||
if (isConnected.value && central != null) {
|
||||
final zero = Uint8List.fromList([Opcode.CONTROLLER_NOTIFICATION.value, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
_peripheralManager.notifyCharacteristic(_central!, _syncTxCharacteristic!, value: zero);
|
||||
await notifyCharacteristic(central!, _syncTxCharacteristic!, value: zero);
|
||||
_sendKeepAlive();
|
||||
}
|
||||
}
|
||||
@@ -347,8 +316,8 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
...bytes,
|
||||
]);
|
||||
|
||||
_peripheralManager.notifyCharacteristic(
|
||||
_central!,
|
||||
await notifyCharacteristic(
|
||||
central!,
|
||||
_asyncCharacteristic!,
|
||||
value: commandProto,
|
||||
);
|
||||
@@ -356,7 +325,7 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
|
||||
if (isKeyUp) {
|
||||
final zero = Uint8List.fromList([Opcode.CONTROLLER_NOTIFICATION.value, 0x08, 0xFF, 0xFF, 0xFF, 0xFF, 0x0F]);
|
||||
_peripheralManager.notifyCharacteristic(_central!, _asyncCharacteristic!, value: zero);
|
||||
await notifyCharacteristic(central!, _asyncCharacteristic!, value: zero);
|
||||
}
|
||||
|
||||
return Success('Sent action: ${keyPair.inGameAction!.name}');
|
||||
@@ -462,14 +431,8 @@ class ZwiftEmulator extends TrainerConnection {
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void cleanup() {
|
||||
_peripheralManager.stopAdvertising();
|
||||
_peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
_isSubscribedToEvents = false;
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
isStarted.value = false;
|
||||
_isLoading = false;
|
||||
super.cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/devices/bluetooth_emulator.dart';
|
||||
import 'package:bike_control/bluetooth/devices/zwift/protocol/zp.pb.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/gen/l10n.dart';
|
||||
@@ -11,19 +11,10 @@ import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/requirements/multi.dart';
|
||||
import 'package:bluetooth_low_energy/bluetooth_low_energy.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
import '../utils/keymap/keymap.dart';
|
||||
|
||||
class RemotePairing extends TrainerConnection {
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
late final _peripheralManager = PeripheralManager();
|
||||
bool _isLoading = false;
|
||||
bool _isServiceAdded = false;
|
||||
bool _isSubscribedToEvents = false;
|
||||
|
||||
Central? _central;
|
||||
class RemotePairing extends BluetoothEmulator {
|
||||
GATTCharacteristic? _inputReport;
|
||||
|
||||
static const String connectionTitle = 'Remote Control';
|
||||
@@ -35,9 +26,9 @@ class RemotePairing extends TrainerConnection {
|
||||
);
|
||||
|
||||
Future<void> reconnect() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
await _peripheralManager.removeAllServices();
|
||||
_isServiceAdded = false;
|
||||
await peripheralManager.stopAdvertising();
|
||||
await peripheralManager.removeAllServices();
|
||||
isServiceAdded = false;
|
||||
startAdvertising().catchError((e) {
|
||||
core.settings.setRemoteControlEnabled(false);
|
||||
core.connection.signalNotification(
|
||||
@@ -47,40 +38,21 @@ class RemotePairing extends TrainerConnection {
|
||||
}
|
||||
|
||||
Future<void> startAdvertising() async {
|
||||
_isLoading = true;
|
||||
isLoading = true;
|
||||
isStarted.value = true;
|
||||
|
||||
_peripheralManager.stateChanged.forEach((state) {
|
||||
print('Peripheral manager state: ${state.state}');
|
||||
});
|
||||
subscribeToStateChanges();
|
||||
subscribeToConnectionStateChanges(null);
|
||||
|
||||
if (!kIsWeb && Platform.isAndroid) {
|
||||
_peripheralManager.connectionStateChanged.forEach((state) {
|
||||
print('Peripheral connection state: ${state.state} of ${state.central.uuid}');
|
||||
if (state.state == ConnectionState.connected) {
|
||||
} else if (state.state == ConnectionState.disconnected) {
|
||||
_central = null;
|
||||
isConnected.value = false;
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, AppLocalizations.current.disconnected),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
final status = await Permission.bluetoothAdvertise.request();
|
||||
if (!status.isGranted) {
|
||||
print('Bluetooth advertise permission not granted');
|
||||
isStarted.value = false;
|
||||
return;
|
||||
}
|
||||
if (!await requestBluetoothAdvertisePermission()) {
|
||||
isStarted.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
while (_peripheralManager.state != BluetoothLowEnergyState.poweredOn && core.settings.getRemoteControlEnabled()) {
|
||||
print('Waiting for peripheral manager to be powered on...');
|
||||
if (!await waitForPoweredOn(() => core.settings.getRemoteControlEnabled())) {
|
||||
if (core.settings.getLastTarget() == Target.thisDevice) {
|
||||
return;
|
||||
}
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
}
|
||||
final inputReport = GATTCharacteristic.mutable(
|
||||
uuid: UUID.fromString('2A4D'),
|
||||
@@ -95,8 +67,8 @@ class RemotePairing extends TrainerConnection {
|
||||
],
|
||||
);
|
||||
|
||||
if (!_isServiceAdded) {
|
||||
await Future.delayed(Duration(seconds: 1));
|
||||
if (!isServiceAdded) {
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
|
||||
final reportMapDataAbsolute = Uint8List.fromList([
|
||||
0x05, 0x01, // Usage Page (Generic Desktop)
|
||||
@@ -206,21 +178,21 @@ class RemotePairing extends TrainerConnection {
|
||||
includedServices: [],
|
||||
);
|
||||
|
||||
if (!_isSubscribedToEvents) {
|
||||
_isSubscribedToEvents = true;
|
||||
_peripheralManager.characteristicReadRequested.forEach((char) {
|
||||
if (!isSubscribedToEvents) {
|
||||
isSubscribedToEvents = true;
|
||||
peripheralManager.characteristicReadRequested.forEach((char) {
|
||||
print('Read request for characteristic: ${char}');
|
||||
// You can respond to read requests here if needed
|
||||
});
|
||||
|
||||
_peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
peripheralManager.characteristicNotifyStateChanged.forEach((char) {
|
||||
if (char.characteristic.uuid == inputReport.uuid) {
|
||||
if (char.state) {
|
||||
_inputReport = char.characteristic;
|
||||
_central = char.central;
|
||||
central = char.central;
|
||||
} else {
|
||||
_inputReport = null;
|
||||
_central = null;
|
||||
central = null;
|
||||
}
|
||||
}
|
||||
print(
|
||||
@@ -228,10 +200,10 @@ class RemotePairing extends TrainerConnection {
|
||||
);
|
||||
});
|
||||
}
|
||||
await _peripheralManager.addService(hidService);
|
||||
await addService(hidService);
|
||||
|
||||
// 3) Optional Battery service
|
||||
await _peripheralManager.addService(
|
||||
await addService(
|
||||
GATTService(
|
||||
uuid: UUID.fromString('180F'),
|
||||
isPrimary: true,
|
||||
@@ -245,7 +217,7 @@ class RemotePairing extends TrainerConnection {
|
||||
includedServices: [],
|
||||
),
|
||||
);
|
||||
_isServiceAdded = true;
|
||||
isServiceAdded = true;
|
||||
}
|
||||
|
||||
final advertisement = Advertisement(
|
||||
@@ -259,20 +231,18 @@ class RemotePairing extends TrainerConnection {
|
||||
);
|
||||
print('Starting advertising with Zwift service...');
|
||||
|
||||
await _peripheralManager.startAdvertising(advertisement);
|
||||
_isLoading = false;
|
||||
await startAdvertising(advertisement);
|
||||
isLoading = false;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopAdvertising() async {
|
||||
await _peripheralManager.stopAdvertising();
|
||||
isStarted.value = false;
|
||||
isConnected.value = false;
|
||||
_isLoading = false;
|
||||
await super.stopAdvertising();
|
||||
}
|
||||
|
||||
Future<void> notifyCharacteristic(Uint8List value) async {
|
||||
if (_inputReport != null && _central != null) {
|
||||
await _peripheralManager.notifyCharacteristic(_central!, _inputReport!, value: value);
|
||||
if (_inputReport != null && central != null) {
|
||||
await peripheralManager.notifyCharacteristic(central!, _inputReport!, value: value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user