mirror of
https://github.com/jonasbark/swiftcontrol.git
synced 2026-02-17 16:07:41 +01:00
openbikecontrol via dircon
This commit is contained in:
39
lib/bluetooth/devices/openbikecontrol/obc_dircon.dart
Normal file
39
lib/bluetooth/devices/openbikecontrol/obc_dircon.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:prop/emulators/dircon/dircon.dart';
|
||||
import 'package:universal_ble/universal_ble.dart';
|
||||
|
||||
abstract class OnMessage {
|
||||
void onMessage(List<int> message);
|
||||
}
|
||||
|
||||
class ObcDircon extends DirCon {
|
||||
final OnMessage onMessageCallback;
|
||||
ObcDircon({required super.socket, required this.onMessageCallback});
|
||||
|
||||
@override
|
||||
List<BleCharacteristic> getCharacteristics(String serviceUUID) {
|
||||
if (serviceUUID.toLowerCase() == OpenBikeControlConstants.SERVICE_UUID) {
|
||||
return [
|
||||
BleCharacteristic(
|
||||
OpenBikeControlConstants.BUTTON_STATE_CHARACTERISTIC_UUID,
|
||||
[CharacteristicProperty.notify],
|
||||
),
|
||||
BleCharacteristic(
|
||||
OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID,
|
||||
[CharacteristicProperty.writeWithoutResponse, CharacteristicProperty.write],
|
||||
),
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@override
|
||||
void processWriteCallback(String characteristicUUID, List<int> characteristicData) {
|
||||
if (characteristicUUID.toLowerCase() == OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID) {
|
||||
onMessageCallback.onMessage(characteristicData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<String> get serviceUUIDs => [OpenBikeControlConstants.SERVICE_UUID];
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/obc_dircon.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/openbikecontrol_device.dart';
|
||||
import 'package:bike_control/bluetooth/devices/openbikecontrol/protocol_parser.dart';
|
||||
import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
|
||||
import 'package:bike_control/bluetooth/messages/notification.dart';
|
||||
import 'package:bike_control/utils/actions/base_actions.dart';
|
||||
import 'package:bike_control/utils/core.dart';
|
||||
import 'package:bike_control/utils/keymap/apps/supported_app.dart';
|
||||
import 'package:bike_control/utils/keymap/buttons.dart';
|
||||
import 'package:bike_control/utils/keymap/keymap.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
@@ -13,7 +15,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:nsd/nsd.dart';
|
||||
import 'package:prop/prop.dart';
|
||||
|
||||
class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
class OpenBikeControlMdnsEmulator extends TrainerConnection implements OnMessage {
|
||||
ServerSocket? _server;
|
||||
Registration? _mdnsRegistration;
|
||||
|
||||
@@ -22,6 +24,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
final ValueNotifier<AppInfo?> connectedApp = ValueNotifier(null);
|
||||
|
||||
Socket? _socket;
|
||||
ObcDircon? _dirCon;
|
||||
|
||||
OpenBikeControlMdnsEmulator()
|
||||
: super(
|
||||
@@ -29,6 +32,9 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
supportedActions: InGameAction.values,
|
||||
);
|
||||
|
||||
bool get _useDirCon =>
|
||||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.dircon) ?? false;
|
||||
|
||||
Future<void> startServer() async {
|
||||
print('Starting mDNS server...');
|
||||
isStarted.value = true;
|
||||
@@ -64,18 +70,23 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
_mdnsRegistration = await register(
|
||||
Service(
|
||||
name: 'BikeControl',
|
||||
type: '_openbikecontrol._tcp',
|
||||
type: _useDirCon ? '_wahoo-fitness-tnp._tcp' : '_openbikecontrol._tcp',
|
||||
port: 36867,
|
||||
//hostName: 'KICKR BIKE SHIFT B84D.local',
|
||||
addresses: [localIP],
|
||||
txt: {
|
||||
'version': Uint8List.fromList([0x01]),
|
||||
'id': Uint8List.fromList('1337'.codeUnits),
|
||||
'name': Uint8List.fromList('BikeControl'.codeUnits),
|
||||
'service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'manufacturer': Uint8List.fromList('OpenBikeControl'.codeUnits),
|
||||
'model': Uint8List.fromList('BikeControl app'.codeUnits),
|
||||
},
|
||||
txt: _useDirCon
|
||||
? {
|
||||
'ble-service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'mac-address': Uint8List.fromList('00:11:22:33:44:55'.codeUnits),
|
||||
'serial-number': Uint8List.fromList('1234567890'.codeUnits),
|
||||
}
|
||||
: {
|
||||
'version': Uint8List.fromList([0x01]),
|
||||
'id': Uint8List.fromList('1337'.codeUnits),
|
||||
'name': Uint8List.fromList('BikeControl'.codeUnits),
|
||||
'service-uuids': Uint8List.fromList(OpenBikeControlConstants.SERVICE_UUID.codeUnits),
|
||||
'manufacturer': Uint8List.fromList('OpenBikeControl'.codeUnits),
|
||||
'model': Uint8List.fromList('BikeControl app'.codeUnits),
|
||||
},
|
||||
),
|
||||
);
|
||||
print('Service: ${_mdnsRegistration!.id} at ${localIP.address}:$_mdnsRegistration');
|
||||
@@ -104,7 +115,7 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
Future<void> _createTcpServer() async {
|
||||
try {
|
||||
_server = await ServerSocket.bind(
|
||||
InternetAddress.anyIPv6,
|
||||
InternetAddress.anyIPv4,
|
||||
36867,
|
||||
shared: true,
|
||||
v6Only: false,
|
||||
@@ -127,33 +138,24 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
print('Client connected: ${socket.remoteAddress.address}:${socket.remotePort}');
|
||||
}
|
||||
|
||||
if (_useDirCon) {
|
||||
_dirCon = ObcDircon(socket: socket, onMessageCallback: this);
|
||||
}
|
||||
|
||||
// Listen for data from the client
|
||||
socket.listen(
|
||||
(List<int> data) {
|
||||
if (kDebugMode) {
|
||||
print('Received message: ${bytesToHex(data)}');
|
||||
}
|
||||
final messageType = data[0];
|
||||
switch (messageType) {
|
||||
case OpenBikeProtocolParser.MSG_TYPE_APP_INFO:
|
||||
try {
|
||||
final appInfo = OpenBikeProtocolParser.parseAppInfo(Uint8List.fromList(data));
|
||||
isConnected.value = true;
|
||||
connectedApp.value = appInfo;
|
||||
|
||||
supportedActions = appInfo.supportedButtons.mapNotNull((b) => b.action).toList();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Connected to app: ${appInfo.appId}'),
|
||||
);
|
||||
} catch (e) {
|
||||
core.connection.signalNotification(LogNotification('Failed to parse app info: $e'));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print('Unknown message type: $messageType');
|
||||
if (_dirCon != null) {
|
||||
_dirCon!.handleIncomingData(data);
|
||||
return;
|
||||
}
|
||||
onMessage(data);
|
||||
},
|
||||
onDone: () {
|
||||
_dirCon = null;
|
||||
SharedLogic.stopKeepAlive();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Disconnected from app: ${connectedApp.value?.appId}'),
|
||||
@@ -207,6 +209,37 @@ class OpenBikeControlMdnsEmulator extends TrainerConnection {
|
||||
|
||||
void _write(Socket socket, List<int> responseData) {
|
||||
debugPrint('Sending response: ${bytesToHex(responseData)}');
|
||||
socket.add(responseData);
|
||||
if (_dirCon != null) {
|
||||
_dirCon!.sendCharacteristicNotification(OpenBikeControlConstants.BUTTON_STATE_CHARACTERISTIC_UUID, responseData);
|
||||
return;
|
||||
} else {
|
||||
socket.add(responseData);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onMessage(List<int> message) {
|
||||
if (kDebugMode) {
|
||||
print('Received message from DirCon: ${bytesToHex(message)}');
|
||||
}
|
||||
final messageType = message[0];
|
||||
switch (messageType) {
|
||||
case OpenBikeProtocolParser.MSG_TYPE_APP_INFO:
|
||||
try {
|
||||
final appInfo = OpenBikeProtocolParser.parseAppInfo(Uint8List.fromList(message));
|
||||
isConnected.value = true;
|
||||
connectedApp.value = appInfo;
|
||||
|
||||
supportedActions = appInfo.supportedButtons.mapNotNull((b) => b.action).toList();
|
||||
core.connection.signalNotification(
|
||||
AlertNotification(LogLevel.LOGLEVEL_INFO, 'Connected to app: ${appInfo.appId}'),
|
||||
);
|
||||
} catch (e) {
|
||||
core.connection.signalNotification(LogNotification('Failed to parse app info: $e'));
|
||||
}
|
||||
break;
|
||||
default:
|
||||
print('Unknown message type: $messageType');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:bike_control/widgets/scan.dart';
|
||||
import 'package:bike_control/widgets/title.dart';
|
||||
import 'package:bike_control/widgets/ui/help_button.dart';
|
||||
import 'package:bike_control/widgets/ui/permissions_list.dart';
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
|
||||
@@ -108,9 +109,10 @@ class _OnboardingPageState extends State<OnboardingPage> {
|
||||
_OnboardingStep.trainer => _TrainerOnboardingStep(
|
||||
onComplete: () {
|
||||
setState(() {
|
||||
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(
|
||||
if (core.settings.getTrainerApp()?.supportsOpenBikeProtocol.containsAny([
|
||||
OpenBikeProtocolSupport.network,
|
||||
) ??
|
||||
OpenBikeProtocolSupport.dircon,
|
||||
]) ??
|
||||
false) {
|
||||
_currentStep = _OnboardingStep.openbikecontrol;
|
||||
} else {
|
||||
|
||||
@@ -170,7 +170,11 @@ class CoreLogic {
|
||||
}
|
||||
|
||||
bool get showObpMdnsEmulator {
|
||||
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol.contains(OpenBikeProtocolSupport.network) == true;
|
||||
return core.settings.getTrainerApp()?.supportsOpenBikeProtocol.containsAny([
|
||||
OpenBikeProtocolSupport.network,
|
||||
OpenBikeProtocolSupport.dircon,
|
||||
]) ==
|
||||
true;
|
||||
}
|
||||
|
||||
bool get showObpBluetoothEmulator {
|
||||
@@ -216,7 +220,8 @@ class CoreLogic {
|
||||
core.settings.getTrainerApp()?.supportsOpenBikeProtocol.isNotEmpty == true;
|
||||
|
||||
bool get showLocalRemoteOptions =>
|
||||
core.actionHandler.supportedModes.isNotEmpty && (showLocalControl || isRemoteControlEnabled || isRemoteKeyboardControlEnabled);
|
||||
core.actionHandler.supportedModes.isNotEmpty &&
|
||||
(showLocalControl || isRemoteControlEnabled || isRemoteKeyboardControlEnabled);
|
||||
|
||||
bool get hasNoConnectionMethod =>
|
||||
!screenshotMode &&
|
||||
|
||||
@@ -10,7 +10,7 @@ class OpenBikeControl extends SupportedApp {
|
||||
packageName: "org.openbikecontrol",
|
||||
compatibleTargets: Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
supportsOpenBikeProtocol: OpenBikeProtocolSupport.values,
|
||||
supportsOpenBikeProtocol: [OpenBikeProtocolSupport.network, OpenBikeProtocolSupport.ble],
|
||||
keymap: Keymap(
|
||||
keyPairs: [],
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'my_whoosh.dart';
|
||||
enum OpenBikeProtocolSupport {
|
||||
ble,
|
||||
network,
|
||||
dircon,
|
||||
}
|
||||
|
||||
abstract class SupportedApp {
|
||||
|
||||
@@ -18,6 +18,7 @@ class TrainingPeaks extends SupportedApp {
|
||||
packageName: "com.indieVelo.client",
|
||||
compatibleTargets: !kIsWeb && Platform.isIOS ? [Target.otherDevice] : Target.values,
|
||||
supportsZwiftEmulation: false,
|
||||
supportsOpenBikeProtocol: [OpenBikeProtocolSupport.ble, OpenBikeProtocolSupport.dircon],
|
||||
star: true,
|
||||
keymap: Keymap(
|
||||
keyPairs: [
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
PODS:
|
||||
- audio_session (0.0.1):
|
||||
- FlutterMacOS
|
||||
- bluetooth_low_energy_darwin (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
@@ -23,6 +25,9 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- ios_receipt (0.0.1):
|
||||
- FlutterMacOS
|
||||
- just_audio (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- keypress_simulator_macos (0.0.1):
|
||||
- FlutterMacOS
|
||||
- media_key_detector_macos (0.0.1):
|
||||
@@ -53,6 +58,7 @@ PODS:
|
||||
- FlutterMacOS
|
||||
|
||||
DEPENDENCIES:
|
||||
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
|
||||
- bluetooth_low_energy_darwin (from `Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin`)
|
||||
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
|
||||
- file_selector_macos (from `Flutter/ephemeral/.symlinks/plugins/file_selector_macos/macos`)
|
||||
@@ -64,6 +70,7 @@ DEPENDENCIES:
|
||||
- in_app_purchase_storekit (from `Flutter/ephemeral/.symlinks/plugins/in_app_purchase_storekit/darwin`)
|
||||
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
|
||||
- ios_receipt (from `Flutter/ephemeral/.symlinks/plugins/ios_receipt/macos`)
|
||||
- just_audio (from `Flutter/ephemeral/.symlinks/plugins/just_audio/darwin`)
|
||||
- keypress_simulator_macos (from `Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos`)
|
||||
- media_key_detector_macos (from `Flutter/ephemeral/.symlinks/plugins/media_key_detector_macos/macos`)
|
||||
- nsd_macos (from `Flutter/ephemeral/.symlinks/plugins/nsd_macos/macos`)
|
||||
@@ -82,6 +89,8 @@ SPEC REPOS:
|
||||
- RevenueCat
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
audio_session:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
|
||||
bluetooth_low_energy_darwin:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/bluetooth_low_energy_darwin/darwin
|
||||
device_info_plus:
|
||||
@@ -104,6 +113,8 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
|
||||
ios_receipt:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/ios_receipt/macos
|
||||
just_audio:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/just_audio/darwin
|
||||
keypress_simulator_macos:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/keypress_simulator_macos/macos
|
||||
media_key_detector_macos:
|
||||
@@ -128,6 +139,7 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audio_session: 728ae3823d914f809c485d390274861a24b0904e
|
||||
bluetooth_low_energy_darwin: 50bc79258e60586e4c4bed5948bd31d925f37fac
|
||||
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
|
||||
file_selector_macos: 3e56eaea051180007b900eacb006686fd54da150
|
||||
@@ -139,6 +151,7 @@ SPEC CHECKSUMS:
|
||||
in_app_purchase_storekit: 2342c0a5da86593124d08dd13d920f39a52b273a
|
||||
in_app_review: 866c9b17c87a7b46a395bda43f5d3ca02deb585a
|
||||
ios_receipt: 8741a75f39e6ca0866313b73c69a5b674cf5c98c
|
||||
just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
|
||||
keypress_simulator_macos: f8556f9101f9f2f175652e0bceddf0fe82a4c6b2
|
||||
media_key_detector_macos: a93757a483b4b47283ade432b1af9e427c47329f
|
||||
nsd_macos: 1a38a38a33adbb396b4c6f303bc076073514cadc
|
||||
|
||||
Reference in New Issue
Block a user