Compare commits

...

8 Commits

Author SHA1 Message Date
Jonas Bark
41a3a8f14d update changelog 2026-01-16 00:21:01 +01:00
Jonas Bark
a883abcd1c Merge remote-tracking branch 'origin/main' 2026-01-16 00:20:40 +01:00
Jonas Bark
ab37de8f40 fix issue #258 2026-01-16 00:20:30 +01:00
jonasbark
ac0e15eaa7 Update instructions for MyWhoosh Link connectivity 2026-01-15 17:06:46 +01:00
Jonas Bark
a6a7e7f0c2 show logs file path 2026-01-15 14:35:18 +01:00
Jonas Bark
3cacdf9a3a update changelog 2026-01-15 14:25:36 +01:00
Jonas Bark
3ebbda3690 kickr headwind: write without response to potentially fix #11 2026-01-15 08:55:03 +01:00
Jonas Bark
74abb13acf skip powermeters from connecting 2026-01-14 12:12:19 +01:00
13 changed files with 72 additions and 16 deletions

View File

@@ -1,3 +1,12 @@
### 4.4.0 (16-01-2026)
**Features**:
- Support for Thinkrider VS200
**Fixes**:
- Android: Local connection method allows passing keyboard events to the trainer app
- macOS: Compatibility with macOS Tahoe
### 4.3.0 (07-01-2026)
**Features**:

View File

@@ -16,6 +16,7 @@ This is a network/local-discovery problem. BikeControl needs the same kind of lo
Checklist:
- Use the MyWhoosh Link app to confirm if "Link" works in general
- Use MyWhoosh Link app and connect, then close it, then open up BikeControl - this is key for some users
- Both devices are on the **same WiFi SSID**
- Avoid “Guest” networks
- Avoid “extenders/mesh guest mode” and networks with device isolation

View File

@@ -59,7 +59,15 @@ abstract class BluetoothDevice extends BaseDevice {
ThinkRiderVs200Constants.SERVICE_UUID,
];
static final List<String> _ignoredNames = ['ASSIOMA', 'QUARQ', 'POWERCRANK'];
static BluetoothDevice? fromScanResult(BleDevice scanResult) {
// skip devices with ignored names
if (scanResult.name != null &&
_ignoredNames.any((ignoredName) => scanResult.name!.toUpperCase().startsWith(ignoredName))) {
return null;
}
// Use the name first as the "System Devices" and Web (android sometimes Windows) don't have manufacturer data
BluetoothDevice? device;
if (kIsWeb) {
@@ -108,8 +116,9 @@ abstract class BluetoothDevice extends BaseDevice {
OpenBikeControlDevice(scanResult),
_ when scanResult.services.contains(WahooKickrHeadwindConstants.SERVICE_UUID.toLowerCase()) =>
WahooKickrHeadwind(scanResult),
_ when scanResult.services.contains(ThinkRiderVs200Constants.SERVICE_UUID.toLowerCase()) =>
ThinkRiderVs200(scanResult),
_ when scanResult.services.contains(ThinkRiderVs200Constants.SERVICE_UUID.toLowerCase()) => ThinkRiderVs200(
scanResult,
),
// otherwise the service UUIDs will be used
_ => null,
};

View File

@@ -80,6 +80,7 @@ class WahooKickrHeadwind extends BluetoothDevice {
service,
characteristic,
manualModeData,
withoutResponse: true,
);
_currentMode = HeadwindMode.manual;
}
@@ -93,6 +94,7 @@ class WahooKickrHeadwind extends BluetoothDevice {
service,
characteristic,
data,
withoutResponse: true,
);
_currentSpeed = speedPercent;
}
@@ -109,6 +111,7 @@ class WahooKickrHeadwind extends BluetoothDevice {
service,
characteristic,
data,
withoutResponse: true,
);
_currentMode = HeadwindMode.heartRate;
}

View File

@@ -109,7 +109,7 @@ Future<void> _persistCrash({
..writeln();
final directory = await _getLogDirectory();
final file = File('${directory.path}/app.logs');
final file = File('${directory.path}/app.log');
final fileLength = await file.length();
if (fileLength > 5 * 1024 * 1024) {
// If log file exceeds 5MB, truncate it

View File

@@ -254,7 +254,7 @@ class _TrainerPageState extends State<TrainerPage> with WidgetsBindingObserver {
SizedBox(height: 4),
Flex(
direction: widget.isMobile ? Axis.vertical : Axis.horizontal,
direction: widget.isMobile || MediaQuery.sizeOf(context).width < 750 ? Axis.vertical : Axis.horizontal,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 8,

View File

@@ -177,10 +177,11 @@ class RevenueCatService {
core.connection.signalNotification(LogNotification('Apple receipt validated for version: $purchasedVersion'));
if (purchasedVersion != null && purchasedVersion.contains(".")) {
final parsedVersion = Version.parse(purchasedVersion);
isPurchasedNotifier.value = parsedVersion < Version(4, 2, 0);
isPurchasedNotifier.value = parsedVersion < Version(4, 2, 0) || parsedVersion >= Version(4, 4, 0);
} else {
final purchasedVersionAsInt = int.tryParse(purchasedVersion.toString()) ?? 1337;
isPurchasedNotifier.value = purchasedVersionAsInt < (Platform.isMacOS ? 61 : 58);
isPurchasedNotifier.value =
purchasedVersionAsInt < (Platform.isMacOS ? 61 : 58) || purchasedVersionAsInt >= 77;
}
}
} else {

View File

@@ -192,22 +192,19 @@ class NotificationRequirement extends PlatformRequirement {
} else {
status = true;
}
if (status) {
await setup();
}
return status;
}
static Future<void> setup() async {
const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings(
'@mipmap/ic_launcher',
);
await core.flutterLocalNotificationsPlugin.initialize(
InitializationSettings(
android: initializationSettingsAndroid,
android: AndroidInitializationSettings(
'@mipmap/ic_launcher',
),
iOS: DarwinInitializationSettings(
requestAlertPermission: false,
requestBadgePermission: false,
requestSoundPermission: false,
),
macOS: DarwinInitializationSettings(
requestAlertPermission: false,

View File

@@ -5,6 +5,7 @@ import 'package:bike_control/bluetooth/devices/gyroscope/gyroscope_steering.dart
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/iap/iap_manager.dart';
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
import 'package:bike_control/utils/requirements/android.dart';
import 'package:bike_control/utils/requirements/multi.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
@@ -25,6 +26,11 @@ class Settings {
Future<String?> init({bool retried = false}) async {
try {
prefs = await SharedPreferences.getInstance();
try {
await NotificationRequirement.setup();
} catch (error, stack) {
recordError(error, stack, context: 'Notification setup');
}
initializeActions(getLastTarget()?.connectionType ?? ConnectionType.unknown);
if (getShowOnboarding() && getTrainerApp() != null) {

View File

@@ -1,9 +1,11 @@
import 'dart:async';
import 'dart:io';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/i18n_extension.dart';
import 'package:bike_control/widgets/ui/toast.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show SelectionArea;
import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
@@ -114,6 +116,17 @@ class _LogviewerState extends State<LogViewer> {
),
),
),
if (!kIsWeb && (Platform.isMacOS || Platform.isWindows || Platform.isLinux))
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
Text('Logs file: '),
SelectableText('${Directory.current.path}/app.log').inlineCode,
],
).small,
),
],
),
);

View File

@@ -176,6 +176,7 @@ class _ConnectionMethodState extends State<ConnectionMethod> with WidgetsBinding
if (widget.supportedActions != null)
Button.outline(
leading: Container(
alignment: Alignment.center,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary,
borderRadius: BorderRadius.circular(12),

View File

@@ -1,7 +1,7 @@
name: bike_control
description: "BikeControl - Control your virtual riding"
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
version: 4.3.1+76
version: 4.4.0+77
environment:
sdk: ^3.9.0

View File

@@ -1,20 +1,25 @@
import 'dart:typed_data';
import 'package:flutter_test/flutter_test.dart';
import 'package:bike_control/bluetooth/devices/bluetooth_device.dart';
import 'package:bike_control/bluetooth/devices/cycplus/cycplus_bc2.dart';
import 'package:bike_control/bluetooth/devices/elite/elite_square.dart';
import 'package:bike_control/bluetooth/devices/elite/elite_sterzo.dart';
import 'package:bike_control/bluetooth/devices/shimano/shimano_di2.dart';
import 'package:bike_control/bluetooth/devices/sram/sram_axs.dart';
import 'package:bike_control/bluetooth/devices/wahoo/wahoo_kickr_bike_shift.dart';
import 'package:bike_control/bluetooth/devices/zwift/constants.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_click.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_clickv2.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_play.dart';
import 'package:bike_control/bluetooth/devices/zwift/zwift_ride.dart';
import 'package:bike_control/utils/actions/base_actions.dart';
import 'package:bike_control/utils/core.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:universal_ble/universal_ble.dart';
void main() {
core.actionHandler = StubActions();
group('Detect Zwift devices', () {
test('Detect Zwift Play', () {
final device = _createBleDevice(
@@ -108,6 +113,17 @@ void main() {
expect(BluetoothDevice.fromScanResult(device), isInstanceOf<ShimanoDi2>());
});
});
group('Skip powermeters', () {
test('Skip Favero Assioma', () {
final device = _createBleDevice(name: 'Assioma 133', services: [SramAxsConstants.SERVICE_UUID]);
expect(BluetoothDevice.fromScanResult(device), isNull);
});
test('Skip QUARQ', () {
final device = _createBleDevice(name: 'QUARQ 133', services: [SramAxsConstants.SERVICE_UUID]);
expect(BluetoothDevice.fromScanResult(device), isNull);
});
});
}
BleDevice _createBleDevice({