From b0df25241a997cfc3f577d54cd7d17fe4b906abc Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Mon, 13 Oct 2025 10:59:12 +0200 Subject: [PATCH] refactor device handling to support more devices #1 --- .github/workflows/build.yml | 1 + lib/bluetooth/devices/base_device.dart | 244 +++--------------- lib/bluetooth/devices/zwift/zwift_device.dart | 212 +++++++++++++++ lib/bluetooth/devices/zwift_click.dart | 9 +- lib/bluetooth/devices/zwift_play.dart | 34 +-- lib/bluetooth/devices/zwift_ride.dart | 42 +-- .../messages/click_notification.dart | 6 +- lib/bluetooth/messages/play_notification.dart | 30 +-- lib/bluetooth/messages/ride_notification.dart | 52 ++-- lib/pages/touch_area.dart | 2 +- lib/utils/actions/android.dart | 2 +- lib/utils/actions/base_actions.dart | 6 +- lib/utils/actions/desktop.dart | 4 +- lib/utils/actions/remote.dart | 4 +- lib/utils/keymap/apps/biketerra.dart | 10 +- lib/utils/keymap/apps/custom_app.dart | 2 +- lib/utils/keymap/apps/my_whoosh.dart | 10 +- lib/utils/keymap/apps/training_peaks.dart | 14 +- lib/utils/keymap/buttons.dart | 4 +- lib/utils/keymap/keymap.dart | 8 +- lib/widgets/custom_keymap_selector.dart | 35 ++- lib/widgets/keymap_explanation.dart | 2 +- lib/widgets/menu.dart | 2 +- test/long_press_test.dart | 22 +- test/percentage_keymap_test.dart | 8 +- 25 files changed, 401 insertions(+), 364 deletions(-) create mode 100644 lib/bluetooth/devices/zwift/zwift_device.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c8f31f..55e5abb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,6 +9,7 @@ env: jobs: build: + if: false name: Build & Release runs-on: macos-latest diff --git a/lib/bluetooth/devices/base_device.dart b/lib/bluetooth/devices/base_device.dart index 0c9448f..fc4cc5e 100644 --- a/lib/bluetooth/devices/base_device.dart +++ b/lib/bluetooth/devices/base_device.dart @@ -9,53 +9,40 @@ import 'package:swift_control/bluetooth/devices/zwift_play.dart'; import 'package:swift_control/bluetooth/devices/zwift_ride.dart'; import 'package:swift_control/main.dart'; import 'package:swift_control/utils/actions/desktop.dart'; -import 'package:swift_control/utils/crypto/local_key_provider.dart'; -import 'package:swift_control/utils/crypto/zap_crypto.dart'; -import 'package:swift_control/utils/single_line_exception.dart'; import 'package:universal_ble/universal_ble.dart'; -import '../../utils/crypto/encryption_utils.dart'; import '../../utils/keymap/buttons.dart'; import '../messages/notification.dart'; abstract class BaseDevice { final BleDevice scanResult; - final List availableButtons; + final List availableButtons; BaseDevice(this.scanResult, {required this.availableButtons}); - final zapEncryption = ZapCrypto(LocalKeyProvider()); - bool isConnected = false; int? batteryLevel; String? firmwareVersion; - bool supportsEncryption = false; - - BleCharacteristic? syncRxCharacteristic; Timer? _longPressTimer; - Set _previouslyPressedButtons = {}; - - List get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK; - String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID; + Set _previouslyPressedButtons = {}; static BaseDevice? fromScanResult(BleDevice scanResult) { // Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data - final device = - kIsWeb - ? switch (scanResult.name) { - 'Zwift Ride' => ZwiftRide(scanResult), - 'Zwift Play' => ZwiftPlay(scanResult), - 'Zwift Click' => ZwiftClickV2(scanResult), - _ => null, - } - : switch (scanResult.name) { - //'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller - // https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/ - 'Zwift Play' => ZwiftPlay(scanResult), - //'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller - _ => null, - }; + final device = kIsWeb + ? switch (scanResult.name) { + 'Zwift Ride' => ZwiftRide(scanResult), + 'Zwift Play' => ZwiftPlay(scanResult), + 'Zwift Click' => ZwiftClickV2(scanResult), + _ => null, + } + : switch (scanResult.name) { + //'Zwift Ride' => ZwiftRide(scanResult), special case for Zwift Ride: we must only connect to the left controller + // https://www.makinolo.com/blog/2024/07/26/zwift-ride-protocol/ + 'Zwift Play' => ZwiftPlay(scanResult), + //'Zwift Click' => ZwiftClick(scanResult), special case for Zwift Click v2: we must only connect to the left controller + _ => null, + }; if (device != null) { return device; @@ -112,165 +99,15 @@ abstract class BaseDevice { } final services = await UniversalBle.discoverServices(device.deviceId); - await _handleServices(services); + await handleServices(services); } - Future _handleServices(List services) async { - final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId); + Future handleServices(List services); + Future processCharacteristic(String characteristic, Uint8List bytes); - if (customService == null) { - throw Exception( - 'Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}.\nYou may need to update the firmware in Zwift Companion app.\nWe found: ${services.joinToString(transform: (s) => s.uuid)}', - ); - } + Future?> processClickNotification(Uint8List message); - final deviceInformationService = services.firstOrNullWhere( - (service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID, - ); - final firmwareCharacteristic = deviceInformationService?.characteristics.firstOrNullWhere( - (c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION, - ); - if (firmwareCharacteristic != null) { - final firmwareData = await UniversalBle.read( - device.deviceId, - deviceInformationService!.uuid, - firmwareCharacteristic.uuid, - ); - firmwareVersion = String.fromCharCodes(firmwareData); - connection.signalChange(this); - } - - final asyncCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_ASYNC_CHARACTERISTIC_UUID, - ); - final syncTxCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID, - ); - syncRxCharacteristic = customService.characteristics.firstOrNullWhere( - (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID, - ); - - if (asyncCharacteristic == null || syncTxCharacteristic == null || syncRxCharacteristic == null) { - throw Exception('Characteristics not found'); - } - - await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid); - await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid); - - await setupHandshake(); - } - - Future setupHandshake() async { - if (supportsEncryption) { - await UniversalBle.write( - device.deviceId, - customServiceId, - syncRxCharacteristic!.uuid, - Uint8List.fromList([ - ...Constants.RIDE_ON, - ...Constants.REQUEST_START, - ...zapEncryption.localKeyProvider.getPublicKeyBytes(), - ]), - withoutResponse: true, - ); - } else { - await UniversalBle.write( - device.deviceId, - customServiceId, - syncRxCharacteristic!.uuid, - Constants.RIDE_ON, - withoutResponse: true, - ); - } - } - - Future processCharacteristic(String characteristic, Uint8List bytes) async { - if (kDebugMode && false) { - print( - "${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}", - ); - } - if (bytes.isEmpty) { - return; - } - - try { - if (bytes.startsWith(startCommand)) { - _processDevicePublicKeyResponse(bytes); - } else if (!supportsEncryption || (bytes.length > Int32List.bytesPerElement + EncryptionUtils.MAC_LENGTH)) { - processData(bytes); - } - } catch (e, stackTrace) { - print("Error processing data: $e"); - print("Stack Trace: $stackTrace"); - if (e is SingleLineException) { - actionStreamInternal.add(LogNotification(e.message)); - } else { - actionStreamInternal.add(LogNotification("$e\n$stackTrace")); - } - } - } - - void _processDevicePublicKeyResponse(Uint8List bytes) { - final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length); - if (kDebugMode) { - print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); - } - zapEncryption.initialise(devicePublicKeyBytes); - } - - Future processData(Uint8List bytes) async { - int type; - Uint8List message; - - if (supportsEncryption) { - final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4 - final payload = bytes.sublist(4); - - if (zapEncryption.encryptionKeyBytes == null) { - actionStreamInternal.add( - LogNotification( - 'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.', - ), - ); - return; - } - - final data = zapEncryption.decrypt(counter, payload); - type = data[0]; - message = data.sublist(1); - } else { - type = bytes[0]; - message = bytes.sublist(1); - } - - switch (type) { - case Constants.EMPTY_MESSAGE_TYPE: - //print("Empty Message"); // expected when nothing happening - break; - case Constants.BATTERY_LEVEL_TYPE: - if (batteryLevel != message[1]) { - batteryLevel = message[1]; - connection.signalChange(this); - } - break; - case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE: - case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE: - case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: - processClickNotification(message) - .then((buttonsClicked) async { - return handleButtonsClicked(buttonsClicked); - }) - .catchError((e) { - actionStreamInternal.add(LogNotification(e.toString())); - }); - break; - } - } - - Future?> processClickNotification(Uint8List message); - - Future handleButtonsClicked(List? buttonsClicked) async { + Future handleButtonsClicked(List? buttonsClicked) async { if (buttonsClicked == null) { // ignore, no changes } else if (buttonsClicked.isEmpty) { @@ -283,7 +120,7 @@ abstract class BaseDevice { buttonsReleased.singleOrNull != null && actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true; if (buttonsReleased.isNotEmpty && isLongPress) { - await _performRelease(buttonsReleased); + await performRelease(buttonsReleased); } _previouslyPressedButtons.clear(); } else { @@ -293,7 +130,7 @@ abstract class BaseDevice { buttonsReleased.singleOrNull != null && actionHandler.supportedApp?.keymap.getKeyPair(buttonsReleased.single)?.isLongPress == true; if (buttonsReleased.isNotEmpty && wasLongPress) { - await _performRelease(buttonsReleased); + await performRelease(buttonsReleased); } final isLongPress = @@ -301,30 +138,26 @@ abstract class BaseDevice { actionHandler.supportedApp?.keymap.getKeyPair(buttonsClicked.single)?.isLongPress == true; if (!isLongPress && - !(buttonsClicked.singleOrNull == ZwiftButton.onOffLeft || - buttonsClicked.singleOrNull == ZwiftButton.onOffRight)) { + !(buttonsClicked.singleOrNull == ControllerButton.onOffLeft || + buttonsClicked.singleOrNull == ControllerButton.onOffRight)) { // we don't want to trigger the long press timer for the on/off buttons, also not when it's a long press key _longPressTimer?.cancel(); _longPressTimer = Timer.periodic(const Duration(milliseconds: 350), (timer) async { - _performClick(buttonsClicked); + performClick(buttonsClicked); }); } // Update currently pressed buttons _previouslyPressedButtons = buttonsClicked.toSet(); if (isLongPress) { - return _performDown(buttonsClicked); + return performDown(buttonsClicked); } else { - return _performClick(buttonsClicked); + return performClick(buttonsClicked); } } } - Future _performDown(List buttonsClicked) async { - if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && - settings.getVibrationEnabled()) { - await _vibrate(); - } + Future performDown(List buttonsClicked) async { for (final action in buttonsClicked) { // For repeated actions, don't trigger key down/up events (useful for long press) actionStreamInternal.add( @@ -333,11 +166,7 @@ abstract class BaseDevice { } } - Future _performClick(List buttonsClicked) async { - if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && - settings.getVibrationEnabled()) { - await _vibrate(); - } + Future performClick(List buttonsClicked) async { for (final action in buttonsClicked) { actionStreamInternal.add( LogNotification(await actionHandler.performAction(action, isKeyDown: true, isKeyUp: true)), @@ -345,7 +174,7 @@ abstract class BaseDevice { } } - Future _performRelease(List buttonsReleased) async { + Future performRelease(List buttonsReleased) async { for (final action in buttonsReleased) { actionStreamInternal.add( LogNotification(await actionHandler.performAction(action, isKeyDown: false, isKeyUp: true)), @@ -353,17 +182,6 @@ abstract class BaseDevice { } } - Future _vibrate() async { - final vibrateCommand = Uint8List.fromList([...Constants.VIBRATE_PATTERN, 0x20]); - await UniversalBle.write( - device.deviceId, - customServiceId, - syncRxCharacteristic!.uuid, - supportsEncryption ? zapEncryption.encrypt(vibrateCommand) : vibrateCommand, - withoutResponse: true, - ); - } - Future disconnect() async { _longPressTimer?.cancel(); // Release any held keys in long press mode diff --git a/lib/bluetooth/devices/zwift/zwift_device.dart b/lib/bluetooth/devices/zwift/zwift_device.dart new file mode 100644 index 0000000..46b003a --- /dev/null +++ b/lib/bluetooth/devices/zwift/zwift_device.dart @@ -0,0 +1,212 @@ +import 'dart:async'; + +import 'package:dartx/dartx.dart'; +import 'package:flutter/foundation.dart'; +import 'package:swift_control/bluetooth/ble.dart'; +import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/messages/notification.dart'; +import 'package:swift_control/main.dart'; +import 'package:swift_control/utils/crypto/local_key_provider.dart'; +import 'package:swift_control/utils/crypto/zap_crypto.dart'; +import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/single_line_exception.dart'; +import 'package:universal_ble/universal_ble.dart'; + +import '../../../utils/crypto/encryption_utils.dart'; + +abstract class ZwiftDevice extends BaseDevice { + final zapEncryption = ZapCrypto(LocalKeyProvider()); + + ZwiftDevice(super.scanResult, {required super.availableButtons}); + + bool supportsEncryption = false; + + BleCharacteristic? syncRxCharacteristic; + + List get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK; + String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID; + + @override + Future handleServices(List services) async { + final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId); + + if (customService == null) { + throw Exception( + 'Custom service $customServiceId not found for device $this ${device.name ?? device.rawName}.\nYou may need to update the firmware in Zwift Companion app.\nWe found: ${services.joinToString(transform: (s) => s.uuid)}', + ); + } + + final deviceInformationService = services.firstOrNullWhere( + (service) => service.uuid == BleUuid.DEVICE_INFORMATION_SERVICE_UUID, + ); + final firmwareCharacteristic = deviceInformationService?.characteristics.firstOrNullWhere( + (c) => c.uuid == BleUuid.DEVICE_INFORMATION_CHARACTERISTIC_FIRMWARE_REVISION, + ); + if (firmwareCharacteristic != null) { + final firmwareData = await UniversalBle.read( + device.deviceId, + deviceInformationService!.uuid, + firmwareCharacteristic.uuid, + ); + firmwareVersion = String.fromCharCodes(firmwareData); + connection.signalChange(this); + } + + final asyncCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == BleUuid.ZWIFT_ASYNC_CHARACTERISTIC_UUID, + ); + final syncTxCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_TX_CHARACTERISTIC_UUID, + ); + syncRxCharacteristic = customService.characteristics.firstOrNullWhere( + (characteristic) => characteristic.uuid == BleUuid.ZWIFT_SYNC_RX_CHARACTERISTIC_UUID, + ); + + if (asyncCharacteristic == null || syncTxCharacteristic == null || syncRxCharacteristic == null) { + throw Exception('Characteristics not found'); + } + + await UniversalBle.subscribeNotifications(device.deviceId, customService.uuid, asyncCharacteristic.uuid); + await UniversalBle.subscribeIndications(device.deviceId, customService.uuid, syncTxCharacteristic.uuid); + + await setupHandshake(); + } + + Future setupHandshake() async { + if (supportsEncryption) { + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + Uint8List.fromList([ + ...Constants.RIDE_ON, + ...Constants.REQUEST_START, + ...zapEncryption.localKeyProvider.getPublicKeyBytes(), + ]), + withoutResponse: true, + ); + } else { + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + Constants.RIDE_ON, + withoutResponse: true, + ); + } + } + + @override + Future processCharacteristic(String characteristic, Uint8List bytes) async { + if (kDebugMode && false) { + print( + "${DateTime.now().toString().split(" ").last} Received data on $characteristic: ${bytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}", + ); + } + if (bytes.isEmpty) { + return; + } + + try { + if (bytes.startsWith(startCommand)) { + _processDevicePublicKeyResponse(bytes); + } else if (!supportsEncryption || (bytes.length > Int32List.bytesPerElement + EncryptionUtils.MAC_LENGTH)) { + processData(bytes); + } + } catch (e, stackTrace) { + print("Error processing data: $e"); + print("Stack Trace: $stackTrace"); + if (e is SingleLineException) { + actionStreamInternal.add(LogNotification(e.message)); + } else { + actionStreamInternal.add(LogNotification("$e\n$stackTrace")); + } + } + } + + void _processDevicePublicKeyResponse(Uint8List bytes) { + final devicePublicKeyBytes = bytes.sublist(Constants.RIDE_ON.length + Constants.RESPONSE_START_CLICK.length); + if (kDebugMode) { + print("Device Public Key - ${devicePublicKeyBytes.map((e) => e.toRadixString(16).padLeft(2, '0')).join(' ')}"); + } + zapEncryption.initialise(devicePublicKeyBytes); + } + + Future processData(Uint8List bytes) async { + int type; + Uint8List message; + + if (supportsEncryption) { + final counter = bytes.sublist(0, 4); // Int.SIZE_BYTES is 4 + final payload = bytes.sublist(4); + + if (zapEncryption.encryptionKeyBytes == null) { + actionStreamInternal.add( + LogNotification( + 'Encryption not initialized, yet. You may need to update the firmware of your device with the Zwift Companion app.', + ), + ); + return; + } + + final data = zapEncryption.decrypt(counter, payload); + type = data[0]; + message = data.sublist(1); + } else { + type = bytes[0]; + message = bytes.sublist(1); + } + + switch (type) { + case Constants.EMPTY_MESSAGE_TYPE: + //print("Empty Message"); // expected when nothing happening + break; + case Constants.BATTERY_LEVEL_TYPE: + if (batteryLevel != message[1]) { + batteryLevel = message[1]; + connection.signalChange(this); + } + break; + case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE: + case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE: + case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: + processClickNotification(message) + .then((buttonsClicked) async { + return handleButtonsClicked(buttonsClicked); + }) + .catchError((e) { + actionStreamInternal.add(LogNotification(e.toString())); + }); + break; + } + } + + @override + Future performDown(List buttonsClicked) async { + if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && + settings.getVibrationEnabled()) { + await _vibrate(); + } + return super.performDown(buttonsClicked); + } + + @override + Future performClick(List buttonsClicked) async { + if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) && + settings.getVibrationEnabled()) { + await _vibrate(); + } + return super.performClick(buttonsClicked); + } + + Future _vibrate() async { + final vibrateCommand = Uint8List.fromList([...Constants.VIBRATE_PATTERN, 0x20]); + await UniversalBle.write( + device.deviceId, + customServiceId, + syncRxCharacteristic!.uuid, + supportsEncryption ? zapEncryption.encrypt(vibrateCommand) : vibrateCommand, + withoutResponse: true, + ); + } +} diff --git a/lib/bluetooth/devices/zwift_click.dart b/lib/bluetooth/devices/zwift_click.dart index 6903a07..206f9a3 100644 --- a/lib/bluetooth/devices/zwift_click.dart +++ b/lib/bluetooth/devices/zwift_click.dart @@ -1,16 +1,17 @@ import 'package:flutter/foundation.dart'; -import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; import '../messages/click_notification.dart'; -class ZwiftClick extends BaseDevice { - ZwiftClick(super.scanResult) : super(availableButtons: [ZwiftButton.shiftUpRight, ZwiftButton.shiftDownLeft]); +class ZwiftClick extends ZwiftDevice { + ZwiftClick(super.scanResult) + : super(availableButtons: [ControllerButton.shiftUpRight, ControllerButton.shiftDownLeft]); ClickNotification? _lastClickNotification; @override - Future?> processClickNotification(Uint8List message) async { + Future?> processClickNotification(Uint8List message) async { final ClickNotification clickNotification = ClickNotification(message); if (_lastClickNotification == null || _lastClickNotification != clickNotification) { _lastClickNotification = clickNotification; diff --git a/lib/bluetooth/devices/zwift_play.dart b/lib/bluetooth/devices/zwift_play.dart index aa111a6..71670c7 100644 --- a/lib/bluetooth/devices/zwift_play.dart +++ b/lib/bluetooth/devices/zwift_play.dart @@ -1,30 +1,30 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; -import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; import 'package:swift_control/bluetooth/messages/play_notification.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; import '../ble.dart'; -class ZwiftPlay extends BaseDevice { +class ZwiftPlay extends ZwiftDevice { ZwiftPlay(super.scanResult) : super( availableButtons: [ - ZwiftButton.y, - ZwiftButton.z, - ZwiftButton.a, - ZwiftButton.b, - ZwiftButton.onOffRight, - ZwiftButton.sideButtonRight, - ZwiftButton.paddleRight, - ZwiftButton.navigationUp, - ZwiftButton.navigationLeft, - ZwiftButton.navigationRight, - ZwiftButton.navigationDown, - ZwiftButton.onOffLeft, - ZwiftButton.sideButtonLeft, - ZwiftButton.paddleLeft, + ControllerButton.y, + ControllerButton.z, + ControllerButton.a, + ControllerButton.b, + ControllerButton.onOffRight, + ControllerButton.sideButtonRight, + ControllerButton.paddleRight, + ControllerButton.navigationUp, + ControllerButton.navigationLeft, + ControllerButton.navigationRight, + ControllerButton.navigationDown, + ControllerButton.onOffLeft, + ControllerButton.sideButtonLeft, + ControllerButton.paddleLeft, ], ); @@ -34,7 +34,7 @@ class ZwiftPlay extends BaseDevice { List get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY; @override - Future?> processClickNotification(Uint8List message) async { + Future?> processClickNotification(Uint8List message) async { final PlayNotification clickNotification = PlayNotification(message); if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) { _lastControllerNotification = clickNotification; diff --git a/lib/bluetooth/devices/zwift_ride.dart b/lib/bluetooth/devices/zwift_ride.dart index 94a6f25..258840d 100644 --- a/lib/bluetooth/devices/zwift_ride.dart +++ b/lib/bluetooth/devices/zwift_ride.dart @@ -1,7 +1,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/foundation.dart'; import 'package:protobuf/protobuf.dart' as $pb; -import 'package:swift_control/bluetooth/devices/base_device.dart'; +import 'package:swift_control/bluetooth/devices/zwift/zwift_device.dart'; import 'package:swift_control/bluetooth/devices/zwift_clickv2.dart'; import 'package:swift_control/bluetooth/messages/ride_notification.dart'; import 'package:swift_control/bluetooth/protocol/zp_vendor.pb.dart'; @@ -13,7 +13,7 @@ import '../ble.dart'; import '../messages/notification.dart'; import '../protocol/zp.pb.dart'; -class ZwiftRide extends BaseDevice { +class ZwiftRide extends ZwiftDevice { /// Minimum absolute analog value (0-100) required to trigger paddle button press. /// Values below this threshold are ignored to prevent accidental triggers from /// analog drift or light touches. @@ -22,24 +22,24 @@ class ZwiftRide extends BaseDevice { ZwiftRide(super.scanResult) : super( availableButtons: [ - ZwiftButton.navigationLeft, - ZwiftButton.navigationRight, - ZwiftButton.navigationUp, - ZwiftButton.navigationDown, - ZwiftButton.a, - ZwiftButton.b, - ZwiftButton.y, - ZwiftButton.z, - ZwiftButton.shiftUpLeft, - ZwiftButton.shiftDownLeft, - ZwiftButton.shiftUpRight, - ZwiftButton.shiftDownRight, - ZwiftButton.powerUpLeft, - ZwiftButton.powerUpRight, - ZwiftButton.onOffLeft, - ZwiftButton.onOffRight, - ZwiftButton.paddleLeft, - ZwiftButton.paddleRight, + ControllerButton.navigationLeft, + ControllerButton.navigationRight, + ControllerButton.navigationUp, + ControllerButton.navigationDown, + ControllerButton.a, + ControllerButton.b, + ControllerButton.y, + ControllerButton.z, + ControllerButton.shiftUpLeft, + ControllerButton.shiftDownLeft, + ControllerButton.shiftUpRight, + ControllerButton.shiftDownRight, + ControllerButton.powerUpLeft, + ControllerButton.powerUpRight, + ControllerButton.onOffLeft, + ControllerButton.onOffRight, + ControllerButton.paddleLeft, + ControllerButton.paddleRight, ], ); @@ -202,7 +202,7 @@ class ZwiftRide extends BaseDevice { } @override - Future?> processClickNotification(Uint8List message) async { + Future?> processClickNotification(Uint8List message) async { final RideNotification clickNotification = RideNotification( message, analogPaddleThreshold: analogPaddleThreshold, diff --git a/lib/bluetooth/messages/click_notification.dart b/lib/bluetooth/messages/click_notification.dart index ee529b2..0874817 100644 --- a/lib/bluetooth/messages/click_notification.dart +++ b/lib/bluetooth/messages/click_notification.dart @@ -8,13 +8,13 @@ import '../protocol/zwift.pb.dart'; import 'notification.dart'; class ClickNotification extends BaseNotification { - late List buttonsClicked; + late List buttonsClicked; ClickNotification(Uint8List message) { final status = ClickKeyPadStatus.fromBuffer(message); buttonsClicked = [ - if (status.buttonPlus == PlayButtonStatus.ON) ZwiftButton.shiftUpRight, - if (status.buttonMinus == PlayButtonStatus.ON) ZwiftButton.shiftDownLeft, + if (status.buttonPlus == PlayButtonStatus.ON) ControllerButton.shiftUpRight, + if (status.buttonMinus == PlayButtonStatus.ON) ControllerButton.shiftDownLeft, ]; } diff --git a/lib/bluetooth/messages/play_notification.dart b/lib/bluetooth/messages/play_notification.dart index fb66b34..f67f70b 100644 --- a/lib/bluetooth/messages/play_notification.dart +++ b/lib/bluetooth/messages/play_notification.dart @@ -7,29 +7,29 @@ import 'package:swift_control/utils/keymap/buttons.dart'; import 'package:swift_control/widgets/keymap_explanation.dart'; class PlayNotification extends BaseNotification { - late List buttonsClicked; + late List buttonsClicked; PlayNotification(Uint8List message) { final status = PlayKeyPadStatus.fromBuffer(message); buttonsClicked = [ if (status.rightPad == PlayButtonStatus.ON) ...[ - if (status.buttonYUp == PlayButtonStatus.ON) ZwiftButton.y, - if (status.buttonZLeft == PlayButtonStatus.ON) ZwiftButton.z, - if (status.buttonARight == PlayButtonStatus.ON) ZwiftButton.a, - if (status.buttonBDown == PlayButtonStatus.ON) ZwiftButton.b, - if (status.buttonOn == PlayButtonStatus.ON) ZwiftButton.onOffRight, - if (status.buttonShift == PlayButtonStatus.ON) ZwiftButton.sideButtonRight, - if (status.analogLR.abs() == 100) ZwiftButton.paddleRight, + if (status.buttonYUp == PlayButtonStatus.ON) ControllerButton.y, + if (status.buttonZLeft == PlayButtonStatus.ON) ControllerButton.z, + if (status.buttonARight == PlayButtonStatus.ON) ControllerButton.a, + if (status.buttonBDown == PlayButtonStatus.ON) ControllerButton.b, + if (status.buttonOn == PlayButtonStatus.ON) ControllerButton.onOffRight, + if (status.buttonShift == PlayButtonStatus.ON) ControllerButton.sideButtonRight, + if (status.analogLR.abs() == 100) ControllerButton.paddleRight, ], if (status.rightPad == PlayButtonStatus.OFF) ...[ - if (status.buttonYUp == PlayButtonStatus.ON) ZwiftButton.navigationUp, - if (status.buttonZLeft == PlayButtonStatus.ON) ZwiftButton.navigationLeft, - if (status.buttonARight == PlayButtonStatus.ON) ZwiftButton.navigationRight, - if (status.buttonBDown == PlayButtonStatus.ON) ZwiftButton.navigationDown, - if (status.buttonOn == PlayButtonStatus.ON) ZwiftButton.onOffLeft, - if (status.buttonShift == PlayButtonStatus.ON) ZwiftButton.sideButtonLeft, - if (status.analogLR.abs() == 100) ZwiftButton.paddleLeft, + if (status.buttonYUp == PlayButtonStatus.ON) ControllerButton.navigationUp, + if (status.buttonZLeft == PlayButtonStatus.ON) ControllerButton.navigationLeft, + if (status.buttonARight == PlayButtonStatus.ON) ControllerButton.navigationRight, + if (status.buttonBDown == PlayButtonStatus.ON) ControllerButton.navigationDown, + if (status.buttonOn == PlayButtonStatus.ON) ControllerButton.onOffLeft, + if (status.buttonShift == PlayButtonStatus.ON) ControllerButton.sideButtonLeft, + if (status.analogLR.abs() == 100) ControllerButton.paddleLeft, ], ]; } diff --git a/lib/bluetooth/messages/ride_notification.dart b/lib/bluetooth/messages/ride_notification.dart index 8b64fb0..69640c1 100644 --- a/lib/bluetooth/messages/ride_notification.dart +++ b/lib/bluetooth/messages/ride_notification.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:dartx/dartx.dart'; import 'package:flutter/foundation.dart'; import 'package:swift_control/bluetooth/messages/notification.dart'; @@ -34,8 +32,8 @@ enum _RideButtonMask { } class RideNotification extends BaseNotification { - late List buttonsClicked; - late List analogButtons; + late List buttonsClicked; + late List analogButtons; RideNotification(Uint8List message, {required int analogPaddleThreshold}) { final status = RideKeyPadStatus.fromBuffer(message); @@ -44,23 +42,31 @@ class RideNotification extends BaseNotification { // Process DIGITAL buttons separately buttonsClicked = [ - if (status.buttonMap & _RideButtonMask.LEFT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.navigationLeft, - if (status.buttonMap & _RideButtonMask.RIGHT_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.navigationRight, - if (status.buttonMap & _RideButtonMask.UP_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.navigationUp, - if (status.buttonMap & _RideButtonMask.DOWN_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.navigationDown, - if (status.buttonMap & _RideButtonMask.A_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.a, - if (status.buttonMap & _RideButtonMask.B_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.b, - if (status.buttonMap & _RideButtonMask.Y_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.y, - if (status.buttonMap & _RideButtonMask.Z_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.z, - if (status.buttonMap & _RideButtonMask.SHFT_UP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.shiftUpLeft, - if (status.buttonMap & _RideButtonMask.SHFT_DN_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.shiftDownLeft, - if (status.buttonMap & _RideButtonMask.SHFT_UP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.shiftUpRight, + if (status.buttonMap & _RideButtonMask.LEFT_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.navigationLeft, + if (status.buttonMap & _RideButtonMask.RIGHT_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.navigationRight, + if (status.buttonMap & _RideButtonMask.UP_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.navigationUp, + if (status.buttonMap & _RideButtonMask.DOWN_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.navigationDown, + if (status.buttonMap & _RideButtonMask.A_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.a, + if (status.buttonMap & _RideButtonMask.B_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.b, + if (status.buttonMap & _RideButtonMask.Y_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.y, + if (status.buttonMap & _RideButtonMask.Z_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.z, + if (status.buttonMap & _RideButtonMask.SHFT_UP_L_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.shiftUpLeft, + if (status.buttonMap & _RideButtonMask.SHFT_DN_L_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.shiftDownLeft, + if (status.buttonMap & _RideButtonMask.SHFT_UP_R_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.shiftUpRight, if (status.buttonMap & _RideButtonMask.SHFT_DN_R_BTN.mask == PlayButtonStatus.ON.value) - ZwiftButton.shiftDownRight, - if (status.buttonMap & _RideButtonMask.POWERUP_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.powerUpLeft, - if (status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.powerUpRight, - if (status.buttonMap & _RideButtonMask.ONOFF_L_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.onOffLeft, - if (status.buttonMap & _RideButtonMask.ONOFF_R_BTN.mask == PlayButtonStatus.ON.value) ZwiftButton.onOffRight, + ControllerButton.shiftDownRight, + if (status.buttonMap & _RideButtonMask.POWERUP_L_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.powerUpLeft, + if (status.buttonMap & _RideButtonMask.POWERUP_R_BTN.mask == PlayButtonStatus.ON.value) + ControllerButton.powerUpRight, + if (status.buttonMap & _RideButtonMask.ONOFF_L_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.onOffLeft, + if (status.buttonMap & _RideButtonMask.ONOFF_R_BTN.mask == PlayButtonStatus.ON.value) ControllerButton.onOffRight, ]; // Process ANALOG inputs separately - now properly separated from digital @@ -71,9 +77,9 @@ class RideNotification extends BaseNotification { if (paddle.hasLocation() && paddle.hasAnalogValue()) { if (paddle.analogValue.abs() >= analogPaddleThreshold) { final button = switch (paddle.location.value) { - 0 => ZwiftButton.paddleLeft, // L0 = left paddle - 1 => ZwiftButton.paddleRight, // L1 = right paddle - _ => null, // L2, L3 unused + 0 => ControllerButton.paddleLeft, // L0 = left paddle + 1 => ControllerButton.paddleRight, // L1 = right paddle + _ => null, // L2, L3 unused }; if (button != null) { diff --git a/lib/pages/touch_area.dart b/lib/pages/touch_area.dart index 30a68cc..d0b1367 100644 --- a/lib/pages/touch_area.dart +++ b/lib/pages/touch_area.dart @@ -35,7 +35,7 @@ class TouchAreaSetupPage extends StatefulWidget { class _TouchAreaSetupPageState extends State { File? _backgroundImage; late StreamSubscription _actionSubscription; - ZwiftButton? _pressedButton; + ControllerButton? _pressedButton; final TransformationController _transformationController = TransformationController(); late Rect _imageRect; diff --git a/lib/utils/actions/android.dart b/lib/utils/actions/android.dart index c7a2740..d2f9efd 100644 --- a/lib/utils/actions/android.dart +++ b/lib/utils/actions/android.dart @@ -25,7 +25,7 @@ class AndroidActions extends BaseActions { } @override - Future performAction(ZwiftButton button, {bool isKeyDown = true, bool isKeyUp = false}) async { + Future performAction(ControllerButton button, {bool isKeyDown = true, bool isKeyUp = false}) async { if (supportedApp == null) { return ("Could not perform ${button.name.splitByUpperCase()}: No keymap set"); } diff --git a/lib/utils/actions/base_actions.dart b/lib/utils/actions/base_actions.dart index e8c1dea..4ff1aee 100644 --- a/lib/utils/actions/base_actions.dart +++ b/lib/utils/actions/base_actions.dart @@ -17,7 +17,7 @@ abstract class BaseActions { this.supportedApp = supportedApp; } - Offset resolveTouchPosition({required ZwiftButton action, required WindowEvent? windowInfo}) { + Offset resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) { final keyPair = supportedApp!.keymap.getKeyPair(action); if (keyPair != null && keyPair.touchPosition != Offset.zero) { // convert relative position to absolute position based on window info @@ -38,14 +38,14 @@ abstract class BaseActions { return Offset.zero; } - Future performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}); + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}); } class StubActions extends BaseActions { StubActions({super.supportedModes = const []}); @override - Future performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) { + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) { return Future.value(action.name); } } diff --git a/lib/utils/actions/desktop.dart b/lib/utils/actions/desktop.dart index dfe925b..1585a40 100644 --- a/lib/utils/actions/desktop.dart +++ b/lib/utils/actions/desktop.dart @@ -9,7 +9,7 @@ class DesktopActions extends BaseActions { // Track keys that are currently held down in long press mode @override - Future performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { if (supportedApp == null) { return ('Supported app is not set'); } @@ -49,7 +49,7 @@ class DesktopActions extends BaseActions { } // Release all held keys (useful for cleanup) - Future releaseAllHeldKeys(List list) async { + Future releaseAllHeldKeys(List list) async { for (final action in list) { final keyPair = supportedApp?.keymap.getKeyPair(action); if (keyPair?.physicalKey != null) { diff --git a/lib/utils/actions/remote.dart b/lib/utils/actions/remote.dart index a95ffee..0857e86 100644 --- a/lib/utils/actions/remote.dart +++ b/lib/utils/actions/remote.dart @@ -16,7 +16,7 @@ class RemoteActions extends BaseActions { RemoteActions({super.supportedModes = const [SupportedMode.touch]}); @override - Future performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { + Future performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) async { if (supportedApp == null) { return 'Supported app is not set'; } @@ -43,7 +43,7 @@ class RemoteActions extends BaseActions { } @override - Offset resolveTouchPosition({required ZwiftButton action, required WindowEvent? windowInfo}) { + Offset resolveTouchPosition({required ControllerButton action, required WindowEvent? windowInfo}) { // for remote actions we use the relative position only final keyPair = supportedApp!.keymap.getKeyPair(action); if (keyPair != null && keyPair.touchPosition != Offset.zero) { diff --git a/lib/utils/keymap/apps/biketerra.dart b/lib/utils/keymap/apps/biketerra.dart index 565fe94..df8ac79 100644 --- a/lib/utils/keymap/apps/biketerra.dart +++ b/lib/utils/keymap/apps/biketerra.dart @@ -13,27 +13,27 @@ class Biketerra extends SupportedApp { keymap: Keymap( keyPairs: [ KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), physicalKey: PhysicalKeyboardKey.keyS, logicalKey: LogicalKeyboardKey.keyS, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), physicalKey: PhysicalKeyboardKey.keyW, logicalKey: LogicalKeyboardKey.keyW, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), physicalKey: PhysicalKeyboardKey.arrowRight, logicalKey: LogicalKeyboardKey.arrowRight, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), physicalKey: PhysicalKeyboardKey.arrowLeft, logicalKey: LogicalKeyboardKey.arrowLeft, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), physicalKey: PhysicalKeyboardKey.keyU, logicalKey: LogicalKeyboardKey.keyU, ), diff --git a/lib/utils/keymap/apps/custom_app.dart b/lib/utils/keymap/apps/custom_app.dart index ac6e0f4..24dc96c 100644 --- a/lib/utils/keymap/apps/custom_app.dart +++ b/lib/utils/keymap/apps/custom_app.dart @@ -35,7 +35,7 @@ class CustomApp extends SupportedApp { } void setKey( - ZwiftButton zwiftButton, { + ControllerButton zwiftButton, { required PhysicalKeyboardKey? physicalKey, required LogicalKeyboardKey? logicalKey, bool isLongPress = false, diff --git a/lib/utils/keymap/apps/my_whoosh.dart b/lib/utils/keymap/apps/my_whoosh.dart index 003c436..a968f55 100644 --- a/lib/utils/keymap/apps/my_whoosh.dart +++ b/lib/utils/keymap/apps/my_whoosh.dart @@ -13,33 +13,33 @@ class MyWhoosh extends SupportedApp { keymap: Keymap( keyPairs: [ KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), physicalKey: PhysicalKeyboardKey.keyI, logicalKey: LogicalKeyboardKey.keyI, touchPosition: Offset(80, 94), ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), physicalKey: PhysicalKeyboardKey.keyK, logicalKey: LogicalKeyboardKey.keyK, touchPosition: Offset(98, 94), ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), physicalKey: PhysicalKeyboardKey.keyD, logicalKey: LogicalKeyboardKey.keyD, touchPosition: Offset(98, 80), isLongPress: true, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, touchPosition: Offset(32, 80), isLongPress: true, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), physicalKey: PhysicalKeyboardKey.keyH, logicalKey: LogicalKeyboardKey.keyH, ), diff --git a/lib/utils/keymap/apps/training_peaks.dart b/lib/utils/keymap/apps/training_peaks.dart index 220f3d9..a03160a 100644 --- a/lib/utils/keymap/apps/training_peaks.dart +++ b/lib/utils/keymap/apps/training_peaks.dart @@ -14,39 +14,39 @@ class TrainingPeaks extends SupportedApp { keyPairs: [ // https://help.trainingpeaks.com/hc/en-us/articles/31340399556877-TrainingPeaks-Virtual-Controls-and-Keyboard-Shortcuts KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftDown).toList(), physicalKey: PhysicalKeyboardKey.numpadSubtract, logicalKey: LogicalKeyboardKey.numpadSubtract, touchPosition: Offset(50 * 1.32, 74), ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.shiftUp).toList(), physicalKey: PhysicalKeyboardKey.numpadAdd, logicalKey: LogicalKeyboardKey.numpadAdd, touchPosition: Offset(50 * 1.15, 74), ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateRight).toList(), physicalKey: PhysicalKeyboardKey.arrowRight, logicalKey: LogicalKeyboardKey.arrowRight, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.navigateLeft).toList(), physicalKey: PhysicalKeyboardKey.arrowLeft, logicalKey: LogicalKeyboardKey.arrowLeft, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.toggleUi).toList(), physicalKey: PhysicalKeyboardKey.keyH, logicalKey: LogicalKeyboardKey.keyH, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.increaseResistance).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.increaseResistance).toList(), physicalKey: PhysicalKeyboardKey.pageUp, logicalKey: LogicalKeyboardKey.pageUp, ), KeyPair( - buttons: ZwiftButton.values.filter((e) => e.action == InGameAction.decreaseResistance).toList(), + buttons: ControllerButton.values.filter((e) => e.action == InGameAction.decreaseResistance).toList(), physicalKey: PhysicalKeyboardKey.pageDown, logicalKey: LogicalKeyboardKey.pageDown, ), diff --git a/lib/utils/keymap/buttons.dart b/lib/utils/keymap/buttons.dart index e8f34a0..8e955e5 100644 --- a/lib/utils/keymap/buttons.dart +++ b/lib/utils/keymap/buttons.dart @@ -15,7 +15,7 @@ enum InGameAction { } } -enum ZwiftButton { +enum ControllerButton { // left controller navigationUp._(InGameAction.increaseResistance, icon: Icons.keyboard_arrow_up, color: Colors.black), navigationDown._(InGameAction.decreaseResistance, icon: Icons.keyboard_arrow_down, color: Colors.black), @@ -47,7 +47,7 @@ enum ZwiftButton { final InGameAction? action; final Color? color; final IconData? icon; - const ZwiftButton._(this.action, {this.color, this.icon}); + const ControllerButton._(this.action, {this.color, this.icon}); @override String toString() { diff --git a/lib/utils/keymap/keymap.dart b/lib/utils/keymap/keymap.dart index aa969ed..c5709b3 100644 --- a/lib/utils/keymap/keymap.dart +++ b/lib/utils/keymap/keymap.dart @@ -24,12 +24,12 @@ class Keymap { ); } - PhysicalKeyboardKey? getPhysicalKey(ZwiftButton action) { + PhysicalKeyboardKey? getPhysicalKey(ControllerButton action) { // get the key pair by in game action return keyPairs.firstOrNullWhere((element) => element.buttons.contains(action))?.physicalKey; } - KeyPair? getKeyPair(ZwiftButton action) { + KeyPair? getKeyPair(ControllerButton action) { // get the key pair by in game action return keyPairs.firstOrNullWhere((element) => element.buttons.contains(action)); } @@ -40,7 +40,7 @@ class Keymap { } class KeyPair { - final List buttons; + final List buttons; PhysicalKeyboardKey? physicalKey; LogicalKeyboardKey? logicalKey; Offset touchPosition; @@ -117,7 +117,7 @@ class KeyPair { return KeyPair( buttons: decoded['actions'] - .map((e) => ZwiftButton.values.firstWhere((element) => element.name == e)) + .map((e) => ControllerButton.values.firstWhere((element) => element.name == e)) .toList(), logicalKey: decoded.containsKey('logicalKey') && int.parse(decoded['logicalKey']) != 0 ? LogicalKeyboardKey(int.parse(decoded['logicalKey'])) diff --git a/lib/widgets/custom_keymap_selector.dart b/lib/widgets/custom_keymap_selector.dart index 4d3d1bb..11095ac 100644 --- a/lib/widgets/custom_keymap_selector.dart +++ b/lib/widgets/custom_keymap_selector.dart @@ -27,7 +27,7 @@ class _HotKeyListenerState extends State { final FocusNode _focusNode = FocusNode(); KeyDownEvent? _pressedKey; - ZwiftButton? _pressedButton; + ControllerButton? _pressedButton; @override void initState() { @@ -84,24 +84,23 @@ class _HotKeyListenerState extends State { @override Widget build(BuildContext context) { return AlertDialog( - content: - _pressedButton == null - ? Text('Press a button on your Click device') - : KeyboardListener( - focusNode: _focusNode, - autofocus: true, - onKeyEvent: _onKey, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - spacing: 20, - children: [ - Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"), - Text(_formatKey(_pressedKey)), - ], - ), + content: _pressedButton == null + ? Text('Press a button on your Click device') + : KeyboardListener( + focusNode: _focusNode, + autofocus: true, + onKeyEvent: _onKey, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 20, + children: [ + Text("Press a key on your keyboard to assign to ${_pressedButton.toString()}"), + Text(_formatKey(_pressedKey)), + ], ), + ), actions: [TextButton(onPressed: () => Navigator.of(context).pop(_pressedKey), child: Text("OK"))], ); diff --git a/lib/widgets/keymap_explanation.dart b/lib/widgets/keymap_explanation.dart index 290b1da..4535edf 100644 --- a/lib/widgets/keymap_explanation.dart +++ b/lib/widgets/keymap_explanation.dart @@ -153,7 +153,7 @@ class KeyWidget extends StatelessWidget { } class ButtonWidget extends StatelessWidget { - final ZwiftButton button; + final ControllerButton button; final bool big; const ButtonWidget({super.key, required this.button, this.big = false}); diff --git a/lib/widgets/menu.dart b/lib/widgets/menu.dart index eb91560..248015f 100644 --- a/lib/widgets/menu.dart +++ b/lib/widgets/menu.dart @@ -68,7 +68,7 @@ class MenuButton extends StatelessWidget { child: PopupMenuButton( child: Text("Simulate buttons"), itemBuilder: (_) { - return ZwiftButton.values + return ControllerButton.values .map( (e) => PopupMenuItem( child: Text(e.name), diff --git a/test/long_press_test.dart b/test/long_press_test.dart index b7f7ec6..38ea26a 100644 --- a/test/long_press_test.dart +++ b/test/long_press_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/services.dart'; -import 'package:swift_control/utils/keymap/keymap.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:swift_control/utils/keymap/buttons.dart'; +import 'package:swift_control/utils/keymap/keymap.dart'; void main() { group('Long Press KeyPair Tests', () { test('KeyPair should encode and decode isLongPress property', () { // Create a KeyPair with long press enabled final keyPair = KeyPair( - buttons: [ZwiftButton.a], + buttons: [ControllerButton.a], physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, isLongPress: true, @@ -16,14 +16,14 @@ void main() { // Encode the KeyPair final encoded = keyPair.encode(); - + // Decode the KeyPair final decoded = KeyPair.decode(encoded); - + // Verify the decoded KeyPair has the correct properties expect(decoded, isNotNull); expect(decoded!.isLongPress, true); - expect(decoded.buttons, equals([ZwiftButton.a])); + expect(decoded.buttons, equals([ControllerButton.a])); expect(decoded.physicalKey, equals(PhysicalKeyboardKey.keyA)); expect(decoded.logicalKey, equals(LogicalKeyboardKey.keyA)); }); @@ -41,7 +41,7 @@ void main() { // Decode the legacy KeyPair final decoded = KeyPair.decode(legacyEncoded); - + // Verify the decoded KeyPair defaults isLongPress to false expect(decoded, isNotNull); expect(decoded!.isLongPress, false); @@ -49,7 +49,7 @@ void main() { test('KeyPair constructor should default isLongPress to false', () { final keyPair = KeyPair( - buttons: [ZwiftButton.a], + buttons: [ControllerButton.a], physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, ); @@ -59,7 +59,7 @@ void main() { test('KeyPair should correctly encode isLongPress false', () { final keyPair = KeyPair( - buttons: [ZwiftButton.a], + buttons: [ControllerButton.a], physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, isLongPress: false, @@ -67,9 +67,9 @@ void main() { final encoded = keyPair.encode(); final decoded = KeyPair.decode(encoded); - + expect(decoded, isNotNull); expect(decoded!.isLongPress, false); }); }); -} \ No newline at end of file +} diff --git a/test/percentage_keymap_test.dart b/test/percentage_keymap_test.dart index 27718f2..7799a4f 100644 --- a/test/percentage_keymap_test.dart +++ b/test/percentage_keymap_test.dart @@ -7,7 +7,7 @@ void main() { group('Percentage-based Keymap Tests', () { test('Should encode touch position as percentage using fallback screen size', () { final keyPair = KeyPair( - buttons: [ZwiftButton.paddleRight], + buttons: [ControllerButton.paddleRight], physicalKey: null, logicalKey: null, touchPosition: Offset(960, 540), // Center of 1920x1080 fallback @@ -22,7 +22,7 @@ void main() { test('Should encode touch position as percentages with fallback when screen size not available', () { final keyPair = KeyPair( - buttons: [ZwiftButton.paddleRight], + buttons: [ControllerButton.paddleRight], physicalKey: null, logicalKey: null, touchPosition: Offset(960, 540), // Center of 1920x1080 fallback @@ -57,7 +57,7 @@ void main() { test('Should handle zero touch position correctly', () { final keyPair = KeyPair( - buttons: [ZwiftButton.paddleRight], + buttons: [ControllerButton.paddleRight], physicalKey: PhysicalKeyboardKey.keyA, logicalKey: LogicalKeyboardKey.keyA, touchPosition: Offset.zero, @@ -72,7 +72,7 @@ void main() { test('Should encode and decode with fallback screen size', () { final keyPair = KeyPair( - buttons: [ZwiftButton.paddleRight], + buttons: [ControllerButton.paddleRight], physicalKey: null, logicalKey: null, touchPosition: Offset(480, 270), // 25% of 1920x1080