From 8a9f7eabc8b457a2edf19a323143a12ded08f946 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Thu, 30 Jul 2026 16:22:32 +0200 Subject: [PATCH 1/6] Switchover: do *not* fall back to usb report sink. --- right/src/hid/transport.cpp | 11 ++++++++++- right/src/hid/transport.h | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/right/src/hid/transport.cpp b/right/src/hid/transport.cpp index a9699b58d..ed7966a90 100644 --- a/right/src/hid/transport.cpp +++ b/right/src/hid/transport.cpp @@ -127,7 +127,7 @@ static report_sink_t determineSink() LOG_WRN("Can't send report - selected connection is not ready!\n"); Connections_HandleSwitchover(ConnectionId_Invalid, false); if (!Connections_IsReady(CurrentHostConnectionId)) { - return ReportSink_Usb; + return ReportSink_BlackHole; } } @@ -252,6 +252,9 @@ extern "C" errno_t Hid_SendKeyboardReport(const hid_keyboard_report_t *report) TestHooks_CaptureReport(report); Hid_KeyboardReportSentCallback(ReportSink_Usb); break; + case ReportSink_BlackHole: + err = -EHOSTUNREACH; + break; default: LOG_WRN("Unhandled and unexpected switch state!\n"); err = -EHOSTUNREACH; @@ -324,6 +327,9 @@ extern "C" errno_t Hid_SendMouseReport(const hid_mouse_report_t *report) } break; #endif + case ReportSink_BlackHole: + err = -EHOSTUNREACH; + break; default: LOG_WRN("Unhandled and unexpected switch state!\n"); err = -EHOSTUNREACH; @@ -390,6 +396,9 @@ extern "C" errno_t Hid_SendControlsReport(const hid_controls_report_t *report) } break; #endif + case ReportSink_BlackHole: + err = -EHOSTUNREACH; + break; default: LOG_WRN("Unhandled and unexpected switch state!\n"); err = -EHOSTUNREACH; diff --git a/right/src/hid/transport.h b/right/src/hid/transport.h index fc588add8..cac786037 100644 --- a/right/src/hid/transport.h +++ b/right/src/hid/transport.h @@ -20,6 +20,7 @@ typedef enum { ReportSink_BleHid, ReportSink_Dongle, ReportSink_TestSuite, + ReportSink_BlackHole, } report_sink_t; typedef enum From ce960480c64c38bc575d21a805573b3b48164725 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Thu, 30 Jul 2026 16:44:30 +0200 Subject: [PATCH 2/6] Switchovers: fix oob gates. --- device/src/bt_conn.c | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/device/src/bt_conn.c b/device/src/bt_conn.c index 636c57ffa..b6583760b 100644 --- a/device/src/bt_conn.c +++ b/device/src/bt_conn.c @@ -498,17 +498,18 @@ static bool isWanted(struct bt_conn *conn, connection_id_t connectionId, connect bool isSelectedConnection = BtAddrEq(addr, &HostConnection(CurrentHostConnectionId)->bleAddress); bool isSelectedSlotEmpty = Connections_Type(CurrentHostConnectionId) == ConnectionType_Empty; bool isPeerConnection = connectionType == ConnectionType_NusLeft || connectionType == ConnectionType_NusRight; + bool isOobPairingPeer = BtPair_OobPairingInProgress && BtAddrEq(addr, &BtPair_GetRemoteOob()->addr); bool weHaveSlotToSpare = BtConn_UnusedPeripheralConnectionCount() > 1 || !shouldReserveForCurrentConnection(); bool result = false; if (Cfg.Bt_AlwaysAdvertise) { - result = isPeerConnection || isSelectedConnection || isSelectedSlotEmpty || weHaveSlotToSpare; + result = isPeerConnection || isSelectedConnection || isSelectedSlotEmpty || isOobPairingPeer || weHaveSlotToSpare; } else { - result = isPeerConnection || isSelectedConnection || isSelectedSlotEmpty; + result = isPeerConnection || isSelectedConnection || isSelectedSlotEmpty || isOobPairingPeer; } if (!result) { - LOG_INF(" Not wanted: haveSlot: %d, isSelected: %d (selected %d, this %d (%d)), isPeer: %d, isEmptySlot: %d", weHaveSlotToSpare, isSelectedConnection, CurrentHostConnectionId, connectionId, connectionType, isPeerConnection, isSelectedSlotEmpty); + LOG_INF(" Not wanted: haveSlot: %d, isSelected: %d (selected %d, this %d (%d)), isPeer: %d, isEmptySlot: %d, isOobPeer: %d", weHaveSlotToSpare, isSelectedConnection, CurrentHostConnectionId, connectionId, connectionType, isPeerConnection, isSelectedSlotEmpty, isOobPairingPeer); } return result; } From 64f1622f1656d4a215f5a67c531abe66e28b4d29 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Thu, 30 Jul 2026 17:26:38 +0200 Subject: [PATCH 3/6] Switchovers: Don't spam remote wakeup on dongle. --- device/src/messenger.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/device/src/messenger.c b/device/src/messenger.c index fd05da98c..8bad0d283 100644 --- a/device/src/messenger.c +++ b/device/src/messenger.c @@ -227,14 +227,6 @@ static void processSyncablePropertyDongle(device_id_t src, const uint8_t* data, return; } -#if DEVICE_IS_UHK_DONGLE - if (!Connections_IsCurrentHostAwake()) { - // We received a report to relay but our USB host is suspended - ask it - // to wake up instead of just failing to deliver. - USB_RemoteWakeup(); - } -#endif - errno_t ATTR_UNUSED ret = sendDongleReport(propertyId, message); #if DEVICE_IS_UHK_DONGLE From abd902077547bf81642e4788583ae743782d92f8 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 31 Jul 2026 18:17:55 +0200 Subject: [PATCH 4/6] Switchovers: fix usb reconnection. --- device/src/bt_conn.c | 1 - right/src/usb_state.c | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/device/src/bt_conn.c b/device/src/bt_conn.c index b6583760b..8d7a3e7e2 100644 --- a/device/src/bt_conn.c +++ b/device/src/bt_conn.c @@ -1155,7 +1155,6 @@ void BtConn_ReserveConnections() { BtConn_DisconnectAllUnidentified(); uint8_t unusedConnectionCount = BtConn_UnusedPeripheralConnectionCount(); - bool selectedConnectionIsBleHid = Connections_Type(CurrentHostConnectionId) == ConnectionType_BtHid; if (unusedConnectionCount == 0) { disconnectOldestHost(); diff --git a/right/src/usb_state.c b/right/src/usb_state.c index 348a54b78..8ee666cb3 100644 --- a/right/src/usb_state.c +++ b/right/src/usb_state.c @@ -32,7 +32,7 @@ static void recalculateConnectionState(void) { #if DEVICE_IS_UHK_DONGLE StateSync_UpdateProperty(StateSyncPropertyId_DongleHostAwake, &UsbState_Awake); #elif defined(__ZEPHYR__) - Connections_SetStateAsync(ConnectionId_UsbHidRight, UsbState_Awake ? ConnectionState_Ready : ConnectionState_Disconnected); + Connections_SetStateAsync(ConnectionId_UsbHidRight, UsbState_TransportUp ? ConnectionState_Ready : ConnectionState_Disconnected); EventScheduler_Schedule(Timer_GetCurrentTime(), EventSchedulerEvent_PowerModeUpdate, "no host short wakeup"); WIDGET_REFRESH(&TargetWidget); #else From 202f920ceab71656f71ec1572d1128093b338a4d Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 31 Jul 2026 18:46:49 +0200 Subject: [PATCH 5/6] Switchovers: Always pass reports to usb if usb is selected --- right/src/hid/transport.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/right/src/hid/transport.cpp b/right/src/hid/transport.cpp index ed7966a90..aca67e249 100644 --- a/right/src/hid/transport.cpp +++ b/right/src/hid/transport.cpp @@ -127,7 +127,11 @@ static report_sink_t determineSink() LOG_WRN("Can't send report - selected connection is not ready!\n"); Connections_HandleSwitchover(ConnectionId_Invalid, false); if (!Connections_IsReady(CurrentHostConnectionId)) { - return ReportSink_BlackHole; + if (connectionType == ConnectionType_UsbHidRight) { + return ReportSink_Usb; + } else { + return ReportSink_BlackHole; + } } } From 7e6a756a1b06127ae228ff398cbe38532e7562bf Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Mon, 3 Aug 2026 16:51:53 +0200 Subject: [PATCH 6/6] Switchovers: default to true single slot connected implementation. --- device/src/connections.c | 19 ++++++++++++++++++- doc-dev/reference-manual.md | 7 ++++++- right/src/config_manager.c | 1 + right/src/config_manager.h | 1 + right/src/macros/set_command.c | 2 ++ 5 files changed, 28 insertions(+), 2 deletions(-) diff --git a/device/src/connections.c b/device/src/connections.c index 056d912d5..4df72d396 100644 --- a/device/src/connections.c +++ b/device/src/connections.c @@ -454,6 +454,18 @@ static void setDongleToStandby(connection_id_t connectionId) { } } +static void disconnectOldHost(connection_id_t oldConnectionId) { + switch (Connections_Type(oldConnectionId)) { + case ConnectionType_BtHid: + case ConnectionType_NusDongle: + LOG_INF("Switchover: disconnecting the old host %d", oldConnectionId); + BtConn_DisconnectOne(oldConnectionId); + break; + default: + break; + } +} + static void updateLastConnection(connection_id_t lastConnId, connection_id_t newConnId) { if ( LastHostConnectionId != lastConnId @@ -477,7 +489,12 @@ static connection_id_t findReadySwitchoverHost(void) { static void switchOver(connection_id_t connectionId, bool explicitlySelected) { if (connectionId != CurrentHostConnectionId) { - setDongleToStandby(CurrentHostConnectionId); + if (Cfg.Bt_KeepConnectionsAlive || Cfg.Bt_AlwaysAdvertise) { + setDongleToStandby(CurrentHostConnectionId); + } else { + // The old host occupies a peripheral slot that the new host may need. + disconnectOldHost(CurrentHostConnectionId); + } } updateLastConnection(CurrentHostConnectionId, connectionId); diff --git a/doc-dev/reference-manual.md b/doc-dev/reference-manual.md index fb1372e54..f604a1189 100644 --- a/doc-dev/reference-manual.md +++ b/doc-dev/reference-manual.md @@ -192,6 +192,8 @@ COMMAND = set leds.fadeTimeout COMMAND = set leds.{keyBacklightFadeTimeout|keyBacklightFadeBatteryTimeout|displayFadeTimeout|displayFadeBatteryTimeout} COMMAND = set battery.chargeLimit { full | optimizeHealth } COMMAND = set bluetooth.enabled BOOL +COMMAND = set bluetooth.alwaysAdvertise BOOL +COMMAND = set bluetooth.keepConnectionsAlive BOOL COMMAND = set modifierLayerTriggers.{shift|alt|super|ctrl} {left|right|both} COMMAND = ¯oArg. CONDITION = @@ -319,7 +321,6 @@ COMMAND = set leds.alwaysOn BOOL COMMAND = set bluetooth.peripheralConnectionCount INT COMMAND = set bluetooth.minAdvertisingDelay INT COMMAND = set bluetooth.directedAdvertisingAllowed BOOL -COMMAND = set bluetooth.alwaysAdvertise BOOL COMMAND = set devMode BOOL COMMAND = set log.sink.usb BOOL COMMAND = set log.sink.oled BOOL @@ -766,6 +767,10 @@ Key actions can be parametrized with macro arguments. These arguments can be exp - `leds.brightness <0-1 multiple of default (FLOAT)>` allows scaling default brightness. E.g., `0.5` will dim the entire keyboard to half of the default values that are configured in Agent - `leds.fadeTimeout ` will make uhk turn off all leds after the configured interval. (This is an alias that sets all of `{keyBacklightFadeTimeout|keyBacklightFadeBatteryTimeout|displayFadeTimeout|displayFadeBatteryTimeout}`) +- bluetooth: + - `set bluetooth.alwaysAdvertise BOOL` makes uhk keep advertising even when the current host is already connected, as long as there is a free peripheral connection slot. This lets other configured hosts (dongles, ble hids) connect in the background, so that switching to them later is instantaneous. Default is off. Implies `bluetooth.keepConnectionsAlive`. + - `set bluetooth.keepConnectionsAlive BOOL` keeps the old host connected when you switch away from it. By default, switching hosts disconnects the previous ble host or dongle in order to free its peripheral connection slot for the new host. Default is off. + - modifier layer triggers: - `set modifierLayerTriggers.{shift|alt|super|ctrl} {left|right|both}` controls whether modifier layers are triggered by left or right or either of the modifiers. diff --git a/right/src/config_manager.c b/right/src/config_manager.c index 867c3c04f..04875631c 100644 --- a/right/src/config_manager.c +++ b/right/src/config_manager.c @@ -279,6 +279,7 @@ const config_t DefaultCfg = (config_t){ #endif .Bt_Enabled = true, .Bt_AlwaysAdvertise = false, + .Bt_KeepConnectionsAlive = false, .Bt_MaxPeripheralConnections = 3, .Bt_MinAdvertisingDelay = 250, .Bt_DirectedAdvertisingAllowed = false, diff --git a/right/src/config_manager.h b/right/src/config_manager.h index 993fc1d29..5e00d34c4 100644 --- a/right/src/config_manager.h +++ b/right/src/config_manager.h @@ -97,6 +97,7 @@ // bluetooth bool Bt_AlwaysAdvertise; + bool Bt_KeepConnectionsAlive; bool Bt_DirectedAdvertisingAllowed; bool Bt_Enabled; diff --git a/right/src/macros/set_command.c b/right/src/macros/set_command.c index e52c5855c..91a5ca91a 100644 --- a/right/src/macros/set_command.c +++ b/right/src/macros/set_command.c @@ -383,6 +383,8 @@ static macro_variable_t bluetooth(parser_context_t* ctx, set_command_action_t ac #ifdef __ZEPHYR__ BtManager_StartScanningAndAdvertisingAsync(false, "set_command - alwaysAdvertise changed"); #endif + } else if (ConsumeToken(ctx, "keepConnectionsAlive")) { + ASSIGN_BOOL(Cfg.Bt_KeepConnectionsAlive); } else if (ConsumeToken(ctx, "directedAdvertisingAllowed")) { ASSIGN_BOOL(Cfg.Bt_DirectedAdvertisingAllowed); #ifdef __ZEPHYR__