Compare commits

...

3 Commits

Author SHA1 Message Date
Jonas Bark
7f24c27201 update readme 2025-04-06 16:21:14 +02:00
Jonas Bark
51c5e34220 long pressing a button now repeats the action every 250ms until it's released 2025-04-06 14:57:50 +02:00
Jonas Bark
10c2cc64a2 don't build on Readme updates 2025-04-06 14:02:30 +02:00
8 changed files with 62 additions and 33 deletions

View File

@@ -4,6 +4,12 @@ on:
push:
branches:
- main
paths:
- '.github/workflows/**'
- 'lib/**'
- 'accessibility/**'
- 'keypress_simulator/**'
- 'pubspec.yaml'
jobs:
build:

View File

@@ -1,3 +1,6 @@
#### 2.0.1 (2025-04-06)
- long pressing a button will trigger the action again every 250ms
#### 2.0.0 (2025-04-06)
- You can now customize the actions (touches, mouse clicks or keyboard keys) for all buttons on all supported Zwift devices
- now shows the battery level of the connected devices

View File

@@ -4,7 +4,12 @@
## Description
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Primarily useful to perform virtual gear shifting.
With SwiftControl you can control your favorite trainer app using your Zwift Click, Zwift Ride or Zwift Play devices. Here's what you can do with it, depending on your configuration:
- Virtual Gear shifting
- Steering / turning
- adjust workout intensity
- control music on your device
- more? If you can do it via keyboard, mouse or touch, you can do it with SwiftControl
https://github.com/user-attachments/assets/1f81b674-1628-4763-ad66-5f3ed7a3f159
@@ -46,7 +51,7 @@ The app connects to your Zwift device automatically.
- you can also use the mouse to click on a certain part of the screen, or use keyboard shortcuts
## Donate
Please consider donating to support the development of this app.
Please consider donating to support the development of this app :)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://paypal.me/boni)

View File

@@ -14,6 +14,7 @@ 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 {
@@ -26,6 +27,8 @@ abstract class BaseDevice {
bool supportsEncryption = true;
Timer? _longPressTimer;
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_CLICK;
String get customServiceId => BleUuid.ZWIFT_CUSTOM_SERVICE_UUID;
@@ -230,12 +233,31 @@ abstract class BaseDevice {
case Constants.CLICK_NOTIFICATION_MESSAGE_TYPE:
case Constants.PLAY_NOTIFICATION_MESSAGE_TYPE:
case Constants.RIDE_NOTIFICATION_MESSAGE_TYPE: // untested
processClickNotification(message).then((_) {}).catchError((e) {
actionStreamInternal.add(LogNotification(e.toString()));
});
processClickNotification(message)
.then((buttonsClicked) async {
if (buttonsClicked == null) {
// ignore, no changes
} else if (buttonsClicked.isEmpty) {
actionStreamInternal.add(LogNotification('Buttons released'));
_longPressTimer?.cancel();
} else {
_longPressTimer?.cancel();
_longPressTimer = Timer.periodic(const Duration(milliseconds: 250), (timer) async {
for (final action in buttonsClicked) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
});
for (final action in buttonsClicked) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
}
})
.catchError((e) {
actionStreamInternal.add(LogNotification(e.toString()));
});
break;
}
}
Future<void> processClickNotification(Uint8List message);
Future<List<ZwiftButton>?> processClickNotification(Uint8List message);
}

View File

@@ -1,7 +1,6 @@
import 'package:flutter/foundation.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/keymap/buttons.dart';
import '../messages/click_notification.dart';
@@ -11,19 +10,17 @@ class ZwiftClick extends BaseDevice {
ClickNotification? _lastClickNotification;
@override
Future<void> processClickNotification(Uint8List message) async {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
final ClickNotification clickNotification = ClickNotification(message);
if (_lastClickNotification == null || _lastClickNotification != clickNotification) {
_lastClickNotification = clickNotification;
if (clickNotification.buttonsClicked.isNotEmpty) {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
}

View File

@@ -1,10 +1,11 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/play_notification.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../../main.dart';
import '../ble.dart';
import '../messages/notification.dart';
class ZwiftPlay extends BaseDevice {
ZwiftPlay(super.scanResult);
@@ -15,7 +16,7 @@ class ZwiftPlay extends BaseDevice {
List<int> get startCommand => Constants.RIDE_ON + Constants.RESPONSE_START_PLAY;
@override
Future<void> processClickNotification(Uint8List message) async {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
final PlayNotification clickNotification = PlayNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
@@ -24,11 +25,9 @@ class ZwiftPlay extends BaseDevice {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
}

View File

@@ -2,10 +2,9 @@ import 'dart:typed_data';
import 'package:swift_control/bluetooth/devices/base_device.dart';
import 'package:swift_control/bluetooth/messages/ride_notification.dart';
import 'package:swift_control/main.dart';
import 'package:swift_control/utils/keymap/buttons.dart';
import '../ble.dart';
import '../messages/notification.dart';
class ZwiftRide extends BaseDevice {
ZwiftRide(super.scanResult);
@@ -19,19 +18,17 @@ class ZwiftRide extends BaseDevice {
RideNotification? _lastControllerNotification;
@override
Future<void> processClickNotification(Uint8List message) async {
Future<List<ZwiftButton>?> processClickNotification(Uint8List message) async {
final RideNotification clickNotification = RideNotification(message);
if (_lastControllerNotification == null || _lastControllerNotification != clickNotification) {
_lastControllerNotification = clickNotification;
if (clickNotification.buttonsClicked.isNotEmpty) {
actionStreamInternal.add(clickNotification);
}
final buttons = clickNotification.buttonsClicked;
for (final action in buttons) {
actionStreamInternal.add(LogNotification(await actionHandler.performAction(action)));
}
return clickNotification.buttonsClicked;
} else {
return null;
}
}
}

View File

@@ -1,7 +1,7 @@
name: swift_control
description: "SwiftControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 2.0.0+0
version: 2.0.1+0
environment:
sdk: ^3.7.0