refactor device handling to support more devices #1

This commit is contained in:
Jonas Bark
2025-10-13 10:59:12 +02:00
parent 56447743b2
commit b0df25241a
25 changed files with 401 additions and 364 deletions

View File

@@ -9,6 +9,7 @@ env:
jobs:
build:
if: false
name: Build & Release
runs-on: macos-latest

View File

@@ -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<ZwiftButton> availableButtons;
final List<ControllerButton> 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<ZwiftButton> _previouslyPressedButtons = <ZwiftButton>{};
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
Set<ControllerButton> _previouslyPressedButtons = <ControllerButton>{};
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<void> _handleServices(List<BleService> services) async {
final customService = services.firstOrNullWhere((service) => service.uuid == customServiceId);
Future<void> handleServices(List<BleService> services);
Future<void> 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<List<ControllerButton>?> 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<void> 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<void> 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<void> 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<List<ZwiftButton>?> processClickNotification(Uint8List message);
Future<void> handleButtonsClicked(List<ZwiftButton>? buttonsClicked) async {
Future<void> handleButtonsClicked(List<ControllerButton>? 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<void> _performDown(List<ZwiftButton> buttonsClicked) async {
if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
settings.getVibrationEnabled()) {
await _vibrate();
}
Future<void> performDown(List<ControllerButton> 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<void> _performClick(List<ZwiftButton> buttonsClicked) async {
if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
settings.getVibrationEnabled()) {
await _vibrate();
}
Future<void> performClick(List<ControllerButton> 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<void> _performRelease(List<ZwiftButton> buttonsReleased) async {
Future<void> performRelease(List<ControllerButton> 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<void> _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<void> disconnect() async {
_longPressTimer?.cancel();
// Release any held keys in long press mode

View File

@@ -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<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
@override
Future<void> handleServices(List<BleService> 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<void> 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<void> 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<void> 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<void> performDown(List<ControllerButton> buttonsClicked) async {
if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
settings.getVibrationEnabled()) {
await _vibrate();
}
return super.performDown(buttonsClicked);
}
@override
Future<void> performClick(List<ControllerButton> buttonsClicked) async {
if (buttonsClicked.any(((e) => e.action == InGameAction.shiftDown || e.action == InGameAction.shiftUp)) &&
settings.getVibrationEnabled()) {
await _vibrate();
}
return super.performClick(buttonsClicked);
}
Future<void> _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,
);
}
}

View File

@@ -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<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
Future<List<ControllerButton>?> processClickNotification(Uint8List message) async {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;

View File

@@ -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<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY;
@override
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
Future<List<ControllerButton>?> processClickNotification(Uint8List message) async {
final PlayNotification clickNotification = PlayNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;

View File

@@ -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<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
Future<List<ControllerButton>?> processClickNotification(Uint8List message) async {
final RideNotification clickNotification = RideNotification(
message,
analogPaddleThreshold: analogPaddleThreshold,

View File

@@ -8,13 +8,13 @@ import '../protocol/zwift.pb.dart';
import 'notification.dart';
class ClickNotification extends BaseNotification {
late List<ZwiftButton> buttonsClicked;
late List<ControllerButton> 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,
];
}

View File

@@ -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<ZwiftButton> buttonsClicked;
late List<ControllerButton> 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,
],
];
}

View File

@@ -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<ZwiftButton> buttonsClicked;
late List<ZwiftButton> analogButtons;
late List<ControllerButton> buttonsClicked;
late List<ControllerButton> 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) {

View File

@@ -35,7 +35,7 @@ class TouchAreaSetupPage extends StatefulWidget {
class _TouchAreaSetupPageState extends State<TouchAreaSetupPage> {
File? _backgroundImage;
late StreamSubscription<BaseNotification> _actionSubscription;
ZwiftButton? _pressedButton;
ControllerButton? _pressedButton;
final TransformationController _transformationController = TransformationController();
late Rect _imageRect;

View File

@@ -25,7 +25,7 @@ class AndroidActions extends BaseActions {
}
@override
Future<String> performAction(ZwiftButton button, {bool isKeyDown = true, bool isKeyUp = false}) async {
Future<String> performAction(ControllerButton button, {bool isKeyDown = true, bool isKeyUp = false}) async {
if (supportedApp == null) {
return ("Could not perform ${button.name.splitByUpperCase()}: No keymap set");
}

View File

@@ -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<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false});
Future<String> performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false});
}
class StubActions extends BaseActions {
StubActions({super.supportedModes = const []});
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) {
Future<String> performAction(ControllerButton action, {bool isKeyDown = true, bool isKeyUp = false}) {
return Future.value(action.name);
}
}

View File

@@ -9,7 +9,7 @@ class DesktopActions extends BaseActions {
// Track keys that are currently held down in long press mode
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async {
Future<String> 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<void> releaseAllHeldKeys(List<ZwiftButton> list) async {
Future<void> releaseAllHeldKeys(List<ControllerButton> list) async {
for (final action in list) {
final keyPair = supportedApp?.keymap.getKeyPair(action);
if (keyPair?.physicalKey != null) {

View File

@@ -16,7 +16,7 @@ class RemoteActions extends BaseActions {
RemoteActions({super.supportedModes = const [SupportedMode.touch]});
@override
Future<String> performAction(ZwiftButton action, {bool isKeyDown = true, bool isKeyUp = false}) async {
Future<String> 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) {

View File

@@ -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,
),

View File

@@ -35,7 +35,7 @@ class CustomApp extends SupportedApp {
}
void setKey(
ZwiftButton zwiftButton, {
ControllerButton zwiftButton, {
required PhysicalKeyboardKey? physicalKey,
required LogicalKeyboardKey? logicalKey,
bool isLongPress = false,

View File

@@ -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,
),

View File

@@ -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,
),

View File

@@ -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() {

View File

@@ -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<ZwiftButton> buttons;
final List<ControllerButton> buttons;
PhysicalKeyboardKey? physicalKey;
LogicalKeyboardKey? logicalKey;
Offset touchPosition;
@@ -117,7 +117,7 @@ class KeyPair {
return KeyPair(
buttons: decoded['actions']
.map<ZwiftButton>((e) => ZwiftButton.values.firstWhere((element) => element.name == e))
.map<ControllerButton>((e) => ControllerButton.values.firstWhere((element) => element.name == e))
.toList(),
logicalKey: decoded.containsKey('logicalKey') && int.parse(decoded['logicalKey']) != 0
? LogicalKeyboardKey(int.parse(decoded['logicalKey']))

View File

@@ -27,7 +27,7 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
final FocusNode _focusNode = FocusNode();
KeyDownEvent? _pressedKey;
ZwiftButton? _pressedButton;
ControllerButton? _pressedButton;
@override
void initState() {
@@ -84,24 +84,23 @@ class _HotKeyListenerState extends State<HotKeyListenerDialog> {
@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"))],
);

View File

@@ -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});

View File

@@ -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),

View File

@@ -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);
});
});
}
}

View File

@@ -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