Di2: resolve issue #233

This commit is contained in:
Jonas Bark
2026-02-15 21:44:02 +01:00
parent 7a77acaf94
commit 8d056b526e
3 changed files with 67 additions and 15 deletions

View File

@@ -1,3 +1,11 @@
### 4.8.0 (15-02-2026)
**Features**:
- Bluetooth media buttons are now supported on iOS
- Shimano Di2: long press and double clicks are now supported:
- perform steering using long presses
- gear changes are now reflected properly without losing any button presses
### 4.7.0 (04-02-2026)
**Features**:

View File

@@ -4,6 +4,8 @@ import 'package:bike_control/utils/core.dart';
import 'package:bike_control/utils/keymap/buttons.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:prop/emulators/constants.dart';
import 'package:prop/emulators/shared.dart';
import 'package:universal_ble/universal_ble.dart';
import '../bluetooth_device.dart';
@@ -25,7 +27,7 @@ class ShimanoDi2 extends BluetoothDevice {
await UniversalBle.subscribeIndications(device.deviceId, service.uuid, characteristic.uuid);
}
final _lastButtons = <int, int>{};
final _lastButtons = <int, ({int value, _Di2State type})>{};
bool _isInitialized = false;
@override
@@ -33,6 +35,7 @@ class ShimanoDi2 extends BluetoothDevice {
@override
Future<void> processCharacteristic(String characteristic, Uint8List bytes) async {
Logger.info('Received data from $characteristic: ${bytesToReadableHex(bytes)}');
if (characteristic.toLowerCase() == ShimanoDi2Constants.D_FLY_CHANNEL_UUID) {
final channels = bytes.sublist(1);
@@ -40,7 +43,7 @@ class ShimanoDi2 extends BluetoothDevice {
if (!_isInitialized) {
channels.forEachIndexed((int value, int index) {
final readableIndex = index + 1;
_lastButtons[index] = value;
_lastButtons[index] = (value: value, type: _Di2State.released);
getOrAddButton(
'D-Fly Channel $readableIndex',
@@ -51,28 +54,61 @@ class ShimanoDi2 extends BluetoothDevice {
return Future.value();
}
final clickedButtons = <ControllerButton>[];
var actualChange = false;
channels.forEachIndexed((int value, int index) {
final didChange = _lastButtons[index] != value;
_lastButtons[index] = value;
final didChange = _lastButtons[index]?.value != value;
final readableIndex = index + 1;
final button = getOrAddButton(
'D-Fly Channel $readableIndex',
() => ControllerButton('D-Fly Channel $readableIndex', sourceDeviceId: device.deviceId),
);
if (didChange) {
clickedButtons.add(button);
if ((value & 0x10) != 0) {
if (_lastButtons[index]?.type == _Di2State.longPress || _lastButtons[index]?.type == _Di2State.keep) {
// short press is triggered after long press, until it's released later on
_lastButtons[index] = (value: value, type: _Di2State.keep);
Logger.info('Button $readableIndex still long pressed');
} else {
_lastButtons[index] = (value: value, type: _Di2State.shortPress);
actualChange = true;
Logger.info('Button $readableIndex short pressed');
}
} else if ((value & 0x20) != 0) {
_lastButtons[index] = (value: value, type: _Di2State.longPress);
actualChange = true;
Logger.info('Button $readableIndex long pressed');
} else if ((value & 0x40) != 0) {
_lastButtons[index] = (value: value, type: _Di2State.doublePress);
actualChange = true;
Logger.info('Button $readableIndex double pressed');
} else {
_lastButtons[index] = (value: value, type: _Di2State.released);
actualChange = true;
Logger.info('Button $readableIndex released');
}
}
});
if (clickedButtons.isNotEmpty) {
await handleButtonsClickedWithoutLongPressSupport(clickedButtons);
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();
Logger.debug('Buttons to trigger: ${buttonsToTrigger.map((b) => b.name).join(', ')}');
handleButtonsClicked(buttonsToTrigger);
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();
if (doublePress.isNotEmpty) {
Logger.debug('Buttons to still trigger: ${doublePress.map((b) => b.name).join(', ')}');
handleButtonsClicked(doublePress);
}
}
}
return Future.value();
}
@override
@@ -97,3 +133,11 @@ class ShimanoDi2Constants {
static const String D_FLY_CHANNEL_UUID = "00002ac2-5348-494d-414e-4f5f424c4500";
}
enum _Di2State {
shortPress,
longPress,
keep,
doublePress,
released,
}

View File

@@ -378,7 +378,7 @@ class _ButtonEditPageState extends State<ButtonEditPage> {
),
],
if (core.connection.accessories.isNotEmpty || kDebugMode) ...[
if (core.connection.accessories.isNotEmpty) ...[
SizedBox(height: 8),
ColoredTitle(text: 'Accessory Actions'),
Builder(