From 6e692cc42e8ac3a52fa81f643ff6711af9391d06 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 8 May 2026 20:21:54 +0200 Subject: [PATCH 1/3] Throttle usb retries. --- right/src/usb_report_updater.c | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index be8060889..b47ae8547 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -879,6 +879,7 @@ uint32_t UsbReportUpdateCounter; uint32_t UpdateUsbReports_LastUpdateTime = 0; uint32_t lastBasicReportTime = 0; +uint32_t retryThrottleTime = 0; static uint8_t keyboardRetries = 0; static bool keyboardNeedsResending = false; static uint8_t controlsRetries = 0; @@ -947,6 +948,11 @@ static void handleFail(errno_t errorCode) { #endif } +static void setRetryThrottle() { + retryThrottleTime = Timer_GetCurrentTime() + MAX(10, Cfg.KeystrokeDelay); +} + + static void sendActiveReports(bool resending) { bool usbReportsChangedByAction = false; bool usbReportsChangedByAnything = false; @@ -979,6 +985,7 @@ static void sendActiveReports(bool resending) { //This is *not* asynchronously safe as long as multiple reports of different type can be sent at the same time. //TODO: consider making it atomic, or lowering semaphore reset delay keyboardNeedsResending = true; + setRetryThrottle(); UsbReportUpdateSemaphore &= ~UsbReportUpdate_Keyboard; EventVector_Set(EventVector_ResendUsbReports); } else { @@ -1002,6 +1009,7 @@ static void sendActiveReports(bool resending) { if (ShouldResendReport(ret == 0, &controlsRetries)) { reportRetry(ret); controlsNeedsResending = true; + setRetryThrottle(); UsbReportUpdateSemaphore &= ~UsbReportUpdate_Controls; EventVector_Set(EventVector_ResendUsbReports); } else { @@ -1026,6 +1034,7 @@ static void sendActiveReports(bool resending) { if (ShouldResendReport(ret == 0, &mouseRetries)) { reportRetry(ret); mouseNeedsResending = true; + setRetryThrottle(); UsbReportUpdateSemaphore &= ~UsbReportUpdate_Mouse; EventVector_Set(EventVector_ResendUsbReports); } else { @@ -1064,12 +1073,16 @@ static bool blockedByReportThrottle() { blocked = true; } + // If we are retrying too agressively, we may clog some USB hubs, so add a throttle in that case as well. + if (currentTime < retryThrottleTime) { + blockedUntil = MAX(retryThrottleTime, blockedUntil);; + blocked = true; + } + // To reduce mouse latency, don't construct report until we are close enough to transport window. if ((int32_t)(UsbReportWindowEstimate - currentTime) > USB_REPORT_WINDOW_LOOKAHEAD_MS) { uint32_t throttleUntil = UsbReportWindowEstimate - USB_REPORT_WINDOW_LOOKAHEAD_MS; - if (!blocked || throttleUntil > blockedUntil) { - blockedUntil = throttleUntil; - } + blockedUntil = MAX(throttleUntil, blockedUntil);; blocked = true; } if (blocked) { From b3a98e42639de82fbdf2c9f8ee5daa47bfd7c39e Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 8 May 2026 20:39:14 +0200 Subject: [PATCH 2/3] Even when giving up on report send, send _some_ reports when alive again. --- right/src/event_scheduler.c | 3 +++ right/src/event_scheduler.h | 1 + right/src/usb_report_updater.c | 26 ++++++++++++++++---------- 3 files changed, 20 insertions(+), 10 deletions(-) diff --git a/right/src/event_scheduler.c b/right/src/event_scheduler.c index 88071ec4c..1c4c26e25 100644 --- a/right/src/event_scheduler.c +++ b/right/src/event_scheduler.c @@ -246,6 +246,9 @@ static void processEvt(event_scheduler_event_t evt) BtConn_KickHid(); #endif break; + case EventSchedulerEvent_SendUsbReports: + EventVector_Set(EventVector_SendUsbReports); + break; default: return; } diff --git a/right/src/event_scheduler.h b/right/src/event_scheduler.h index ca18febb2..45b0afb1e 100644 --- a/right/src/event_scheduler.h +++ b/right/src/event_scheduler.h @@ -49,6 +49,7 @@ EventSchedulerEvent_UnselectHostConnection, EventSchedulerEvent_OneShotTimeout, EventSchedulerEvent_KickHid, + EventSchedulerEvent_SendUsbReports, EventSchedulerEvent_Count } event_scheduler_event_t; diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index b47ae8547..23107c938 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -66,6 +66,7 @@ LOG_MODULE_REGISTER(UsbReports, LOG_LEVEL_INF); #include "stubs.h" #endif + bool TestUsbStack = false; static key_action_cached_t actionCache[SLOT_COUNT][MAX_KEY_COUNT_PER_MODULE]; @@ -81,6 +82,8 @@ uint32_t UsbReportWindowEstimate = 0; // latency. #define USB_REPORT_WINDOW_LOOKAHEAD_MS 6 +#define USB_RESEND_DELAY_MS MAX(10, Cfg.KeystrokeDelay) + volatile uint8_t UsbReportUpdateSemaphore = 0; // Modifiers can be applied as one of the following classes @@ -932,6 +935,10 @@ static void reportRetry(errno_t err) { } static void handleFail(errno_t errorCode) { + // In any case, make the keyboard wake up, compare the usb reports, and possibly send them (those that are up-to-date at that time) + // This way, we loose some reports along the way, but at least don't produce stuck keys + EventScheduler_Schedule(Timer_GetCurrentTime() + USB_RESEND_DELAY_MS, EventSchedulerEvent_SendUsbReports, "usb-resend"); + #ifdef __ZEPHYR__ if (ActiveHostConnectionId == ConnectionId_Invalid) { LOG_WRN("Send failed: no connection selected: %s\n", ErrToStr(errorCode)); @@ -948,10 +955,6 @@ static void handleFail(errno_t errorCode) { #endif } -static void setRetryThrottle() { - retryThrottleTime = Timer_GetCurrentTime() + MAX(10, Cfg.KeystrokeDelay); -} - static void sendActiveReports(bool resending) { bool usbReportsChangedByAction = false; @@ -985,15 +988,16 @@ static void sendActiveReports(bool resending) { //This is *not* asynchronously safe as long as multiple reports of different type can be sent at the same time. //TODO: consider making it atomic, or lowering semaphore reset delay keyboardNeedsResending = true; - setRetryThrottle(); + retryThrottleTime = Timer_GetCurrentTime() + USB_RESEND_DELAY_MS; UsbReportUpdateSemaphore &= ~UsbReportUpdate_Keyboard; EventVector_Set(EventVector_ResendUsbReports); } else { if (ret != 0) { handleFail(ret); + } else { + switchActiveKeyboardReport(); } keyboardNeedsResending = false; - switchActiveKeyboardReport(); } } usbReportsChangedByAction = true; @@ -1009,15 +1013,16 @@ static void sendActiveReports(bool resending) { if (ShouldResendReport(ret == 0, &controlsRetries)) { reportRetry(ret); controlsNeedsResending = true; - setRetryThrottle(); + retryThrottleTime = Timer_GetCurrentTime() + USB_RESEND_DELAY_MS; UsbReportUpdateSemaphore &= ~UsbReportUpdate_Controls; EventVector_Set(EventVector_ResendUsbReports); } else { if (ret != 0) { handleFail(ret); + } else { + switchActiveControlsReport(); } controlsNeedsResending = false; - switchActiveControlsReport(); } UsbReportUpdater_LastActivityTime = resending ? UsbReportUpdater_LastActivityTime : Timer_GetCurrentTime(); usbReportsChangedByAction = true; @@ -1034,16 +1039,17 @@ static void sendActiveReports(bool resending) { if (ShouldResendReport(ret == 0, &mouseRetries)) { reportRetry(ret); mouseNeedsResending = true; - setRetryThrottle(); + retryThrottleTime = Timer_GetCurrentTime() + USB_RESEND_DELAY_MS; UsbReportUpdateSemaphore &= ~UsbReportUpdate_Mouse; EventVector_Set(EventVector_ResendUsbReports); } else { if (ret != 0) { handleFail(ret); clearMouseMovement(); // Don't make cursor jump if we have connection issues. + } else { + switchActiveMouseReport(); } mouseNeedsResending = false; - switchActiveMouseReport(); } Debug_RecordBleSendResult(ret); From 65204ac71c9532fb7027a368c44a80356777e298 Mon Sep 17 00:00:00 2001 From: Karel Tucek Date: Fri, 8 May 2026 20:59:04 +0200 Subject: [PATCH 3/3] Add usb give up logic to allow increasing report drop delay. --- right/src/usb_report_updater.c | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/right/src/usb_report_updater.c b/right/src/usb_report_updater.c index 23107c938..337b591b9 100644 --- a/right/src/usb_report_updater.c +++ b/right/src/usb_report_updater.c @@ -890,17 +890,23 @@ static bool controlsNeedsResending = false; static uint8_t mouseRetries = 0; static bool mouseNeedsResending = false; -// Try resending a report for 512ms. Give up if it doesn't succeed by then. +// Try resending a report for maxDelay ms. Give up if it doesn't succeed by then. +// Once given up, stay given up until some statusOk is seen, so a full queue doesn't +// freeze for queueLength * maxDelay on replay. bool ShouldResendReport(bool statusOk, uint8_t* counter) { + static bool givenUp = false; if (statusOk) { *counter = 0; + givenUp = false; return false; } - // keep this low, since the actual delay this causes with a full queue is - // queueLength * maxDelay - const uint16_t maxDelay = 128; //ms + if (givenUp) { + return false; + } + + const uint16_t maxDelay = 1024; //ms const uint8_t granularity = 16; //ms uint8_t minimizedTime = Timer_GetCurrentTime() / granularity; @@ -911,6 +917,7 @@ bool ShouldResendReport(bool statusOk, uint8_t* counter) { return true; } else { *counter = 0; + givenUp = true; return false; } } @@ -995,6 +1002,8 @@ static void sendActiveReports(bool resending) { if (ret != 0) { handleFail(ret); } else { + // don't throw out the state. This way even if we drop reports, we keep in-line with what we have actually sent, so + // once the connection is alive again, we report the new state if needed. switchActiveKeyboardReport(); } keyboardNeedsResending = false;