From 8d056b526e1f0250132a1c53a9b1355b71a5142d Mon Sep 17 00:00:00 2001 From: Jonas Bark Date: Sun, 15 Feb 2026 21:44:02 +0100 Subject: [PATCH] Di2: resolve issue #233 --- CHANGELOG.md | 8 +++ .../devices/shimano/shimano_di2.dart | 72 +++++++++++++++---- lib/pages/button_edit.dart | 2 +- 3 files changed, 67 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6658ba8..f923af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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**: diff --git a/lib/bluetooth/devices/shimano/shimano_di2.dart b/lib/bluetooth/devices/shimano/shimano_di2.dart index 61ff617..1dc5fc8 100644 --- a/lib/bluetooth/devices/shimano/shimano_di2.dart +++ b/lib/bluetooth/devices/shimano/shimano_di2.dart @@ -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 = {}; + final _lastButtons = {}; bool _isInitialized = false; @override @@ -33,6 +35,7 @@ class ShimanoDi2 extends BluetoothDevice { @override Future 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 = []; - + 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, +} diff --git a/lib/pages/button_edit.dart b/lib/pages/button_edit.dart index 223b431..33c7719 100644 --- a/lib/pages/button_edit.dart +++ b/lib/pages/button_edit.dart @@ -378,7 +378,7 @@ class _ButtonEditPageState extends State { ), ], - if (core.connection.accessories.isNotEmpty || kDebugMode) ...[ + if (core.connection.accessories.isNotEmpty) ...[ SizedBox(height: 8), ColoredTitle(text: 'Accessory Actions'), Builder(