Compare commits

...

3 Commits

Author SHA1 Message Date
Jonas Bark
02c038daaa additional fixes 2026-02-16 18:32:31 +01:00
Jonas Bark
05352d7118 additional fixes 2026-02-16 18:22:16 +01:00
Jonas Bark
5c7e8b923b fix shimano di2 implementation 2026-02-16 17:58:10 +01:00
4 changed files with 85 additions and 53 deletions

View File

@@ -7,7 +7,6 @@ import 'package:bike_control/bluetooth/devices/trainer_connection.dart';
import 'package:bike_control/bluetooth/messages/notification.dart' show AlertNotification, LogNotification;
import 'package:bike_control/utils/actions/base_actions.dart';
import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/keymap/apps/training_peaks.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:bike_control/utils/keymap/keymap.dart';
import 'package:bike_control/widgets/title.dart';
@@ -111,7 +110,7 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
_peripheralManager.characteristicWriteRequested.forEach((eventArgs) async {
final characteristic = eventArgs.characteristic;
final request = eventArgs.request;
var value = request.value;
final value = request.value;
if (kDebugMode) {
print('Write request for characteristic: ${characteristic.uuid}: ${bytesToReadableHex(value)}');
}
@@ -119,15 +118,12 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
switch (eventArgs.characteristic.uuid.toString().toLowerCase()) {
case OpenBikeControlConstants.APPINFO_CHARACTERISTIC_UUID:
try {
if (core.settings.getTrainerApp() is TrainingPeaks) {
if (firstAppInfoMessage == null) {
firstAppInfoMessage = value;
return;
} else {
value = Uint8List.fromList([...firstAppInfoMessage!, ...value]);
}
}
final appInfo = OpenBikeProtocolParser.parseAppInfo(value);
// use this fallback if first message is incomplete (e.g. TrainingPeaks on macOS)
AppInfo appInfo = OpenBikeProtocolParser.parseAppInfo(
Uint8List.fromList([...?firstAppInfoMessage, ...value]),
);
firstAppInfoMessage = null;
isConnected.value = true;
connectedApp.value = appInfo;
supportedActions = appInfo.supportedButtons.mapNotNull((b) => b.action).toList();
@@ -137,6 +133,10 @@ class OpenBikeControlBluetoothEmulator extends TrainerConnection {
core.connection.signalNotification(LogNotification('Parsed App Info: $appInfo'));
} catch (e) {
core.connection.signalNotification(LogNotification('Error parsing App Info ${bytesToHex(value)}: $e'));
if (firstAppInfoMessage == null) {
firstAppInfoMessage = value;
return;
}
}
break;
default:

View File

@@ -87,29 +87,57 @@ class ShimanoDi2 extends BluetoothDevice {
});
if (actualChange) {
final buttonsToTrigger = _lastButtons.entries
.where((entry) {
final type = entry.value.type;
return type != _Di2State.released;
})
.map((entry) => availableButtons.firstWhere((button) => button.name == 'D-Fly Channel ${entry.key + 1}'))
.toList();
final Map<_Di2State, List<ControllerButton>> mapped = _lastButtons.entries.groupBy((e) => e.value.type).map((
key,
value,
) {
final buttons = value
.map((entry) => availableButtons.firstWhere((button) => button.name == 'D-Fly Channel ${entry.key + 1}'))
.toList();
return MapEntry(key, buttons);
});
Logger.debug('Buttons to trigger: ${buttonsToTrigger.map((b) => b.name).join(', ')}');
handleButtonsClicked(buttonsToTrigger);
final shortPress = [...?mapped[_Di2State.shortPress], ...?mapped[_Di2State.doublePress]];
if (shortPress.isNotEmpty) {
Logger.debug('Short Press Buttons to trigger: ${shortPress.map((b) => b.name).join(', ')}');
handleButtonsClicked(shortPress);
handleButtonsClicked([]);
_resetButtonsForState([_Di2State.shortPress]);
}
final doublePress = _lastButtons.entries
.filter((entry) => entry.value.type == _Di2State.doublePress)
.map((entry) => availableButtons.firstWhere((button) => button.name == 'D-Fly Channel ${entry.key + 1}'))
.toList();
final longPress = mapped[_Di2State.longPress] ?? [];
if (longPress.isNotEmpty) {
Logger.debug('Long Press Buttons to trigger: ${longPress.map((b) => b.name).join(', ')}');
handleButtonsClicked(longPress);
}
final released = mapped[_Di2State.released] ?? [];
final keepPress = mapped[_Di2State.longPress] ?? [];
if (released.isNotEmpty && keepPress.isEmpty) {
Logger.debug('Releasing all Buttons');
handleButtonsClicked([]);
}
final doublePress = mapped[_Di2State.doublePress] ?? [];
if (doublePress.isNotEmpty) {
Logger.debug('Buttons to still trigger: ${doublePress.map((b) => b.name).join(', ')}');
handleButtonsClicked(doublePress);
handleButtonsClicked([]);
_resetButtonsForState([_Di2State.doublePress]);
}
}
}
}
void _resetButtonsForState(List<_Di2State> list) {
_lastButtons.forEach((key, value) {
if (list.contains(value.type)) {
_lastButtons[key] = (value: value.value, type: _Di2State.released);
}
});
}
@override
Widget showInformation(BuildContext context) {
return Column(

View File

@@ -237,7 +237,7 @@ class _ConnectionMethodState extends State<ConnectionMethod> with WidgetsBinding
Future.wait(widget.requirements.map((e) => e.getStatus())).then((result) {
final allDone = result.every((e) => e);
if (context.mounted) {
if (context.mounted && widget.isEnabled != allDone) {
widget.onChange(allDone);
}
});

View File

@@ -14,6 +14,7 @@ void buildToast({
Duration? duration,
}) {
if (navigatorKey.currentContext?.mounted ?? false) {
final isMobile = MediaQuery.sizeOf(navigatorKey.currentContext!).width < 600;
showToast(
context: navigatorKey.currentContext!,
location: location,
@@ -24,34 +25,37 @@ void buildToast({
LogLevel.LOGLEVEL_ERROR => duration ?? const Duration(seconds: 7),
_ => duration ?? const Duration(seconds: 3),
},
builder: (context, overlay) => SurfaceCard(
filled: switch (level) {
LogLevel.LOGLEVEL_WARNING => true,
LogLevel.LOGLEVEL_ERROR => true,
_ => false,
},
fillColor: switch (level) {
LogLevel.LOGLEVEL_DEBUG => null,
LogLevel.LOGLEVEL_INFO => null,
LogLevel.LOGLEVEL_WARNING => Theme.of(context).colorScheme.chart1,
LogLevel.LOGLEVEL_ERROR => Theme.of(context).colorScheme.destructive,
_ => null,
},
child: Basic(
title: titleWidget ?? Text(title ?? ''),
subtitle: subtitle != null ? Text(subtitle) : null,
trailing: titleWidget is ButtonWidget
? null
: PrimaryButton(
size: ButtonSize.small,
onPressed: () {
// Close the toast programmatically when clicking Undo.
overlay.close();
onClose?.call();
},
child: Text(closeTitle),
),
trailingAlignment: Alignment.center,
builder: (context, overlay) => Container(
margin: EdgeInsets.only(bottom: isMobile ? 50 : 0),
child: SurfaceCard(
filled: switch (level) {
LogLevel.LOGLEVEL_WARNING => true,
LogLevel.LOGLEVEL_ERROR => true,
_ => false,
},
fillColor: switch (level) {
LogLevel.LOGLEVEL_DEBUG => null,
LogLevel.LOGLEVEL_INFO => null,
LogLevel.LOGLEVEL_WARNING => Theme.of(context).colorScheme.chart1,
LogLevel.LOGLEVEL_ERROR => Theme.of(context).colorScheme.destructive,
_ => null,
},
child: Basic(
title: titleWidget ?? Text(title ?? ''),
subtitle: subtitle != null ? Text(subtitle) : null,
trailing: titleWidget is ButtonWidget
? null
: PrimaryButton(
size: ButtonSize.small,
onPressed: () {
// Close the toast programmatically when clicking Undo.
overlay.close();
onClose?.call();
},
child: Text(closeTitle),
),
trailingAlignment: Alignment.center,
),
),
),
);