Just quickly:
I am a UIUX designer and a big UI customisation nerd, I have used BTT for more than a decade as my main UI customisation tool. Whilst everyone else on the planet was hating the touch bar, I loved it because of BTT.
Bravo Andreas, I probably wouldn't have gotten into this profession without your software.
Note on Logitech:
I have always loved Logi hardware and loathed their configuration software (Logi Options and Options+) for the same reasons most probably do, mainly because of their custom HID signals not being picked up in other software. In order to customise the i/o, I have set mouse buttons set to trigger various Function keys in LogiOptions+ and then set all kinds of triggers for the function keys in BTT (Keyboard shortcuts and Key Sequences). Also needed Karabiner-elements for some customisation. This is a mess to set up and troubleshoot, but that's what i've been stably running for a while.
I was very excited to see that BTT is supporting the Logi HID protocol, and that it meant deleting LogiOptions+. I set aside a few hours to delete LogiOptions+ and Karabiner-Elements and set everything up in BTT. Aside from a bit of JS to replicate the logi gestures, everything has worked beautifully. The only problem i've had is the configuration and all of my customisations & macros breaking on a pretty regular basis.
I have then spent quite a bit of time trying to pinpoint the issues. I've created instrumentation and tested a bunch of hypotheses. I have created JS pollers to detect issues and repair scripts to fix them. However now i'm starting to get crash loop when running the pollers and the repair scripts, i think there is an issue when I manually call Logitech JS functions in a certain way. I am unsure whether the issue is in my implementation or with the BTT Logitech Bridge.
At this point, it's either revert to using LogiOptions + Karabiner-Elements + BTT, or I ask the expert for help. I will list out the details below. I have also attached a TEST_LOG, apologies there is a lot there.
TEST_LOG_FOR_DEBUG_REPORT.pdf (738.0 KB)
I figure instead of giving you all of the JS i used, i'll give you this and let you request what you need from me.
P.S - I prefer to use the bolt receiver rather than bluetooth - when 3 or more bluetooth devices are connected to this mac, I get bandwidth issues with my BT headphones.
Bug Summary
BetterTouchTool (BTT) intermittently fails to apply Logitech MX Master 3S configuration after reconnect events (sleep/wake, Easy-Switch away/back, cold boot/login). When this happens, diverted button macros stop firing and/or DPI drops to 1000 (cursor ~4x slower). The issue often does not self-recover even after 10+ minutes and is reliably fixed by restarting BTT.
Environment
- macOS: Sequoia 15.6.1
- Device: Logitech MX Master 3S via Logi Bolt receiver
- Keyboard: MX Keys via Bluetooth
- Conflicting software:
- Logitech Options+ - uninstalled
- Karabiner Elements - uninstalled
- BTT: Version 6.051 - Logitech HID++ 2.0 support (Also a few previous versions, i've been updating to alpha every couple of days)
Symptoms
- BTT macros do not fire for diverted buttons.
- Pointer speed/DPI becomes unusably slow (reads as 1000 vs target 4000).
- Some failures self-recover after ~8-16s; others do not recover without BTT restart.
- Restarting BTT restores functionality; this is needed multiple times per day.
Steps to reproduce
Scenario A: Easy-Switch away/back
- Switch mouse to another computer via Easy-Switch.
- Wait 5-15 seconds.
- Switch back to this Mac.
Scenario B: Sleep/Wake
- Put Mac to sleep.
- Wait ~15-20 seconds.
- Wake Mac.
- Repeat multiple cycles (failures observed after 2-4 cycles).
Expected
- BTT Logitech configuration (DPI + diverted buttons/macros) should be applied immediately and reliably after reconnect/switch-back.
Actual
- BTT Logitech configuration intermittently fails to apply.
- Macros stop firing for diverted buttons.
- DPI sets to 1000, cursor moves ~4x slower.
- Failures can persist for minutes with no recovery.
- Restarting BTT restores correct behaviour.
Instrumentation and testing performed
Created BTT triggers (Named Triggers, Run Real JavaScript):
LOGI_POLLLOGI_SNAPSHOTLOGI_MARKLOGI_EVENT_PROBE- Diagnostics:
LOGI_DIAGNOSTICLOGI_CHECK_MACROS
Trigger wiring in BTT:
LOGI_POLLattached to "Trigger On Script Output Change" (2s interval)- later version of the poller was refactored and attached to Time Based trigger (2s)
LOGI_MARKattached to a keyboard shortcut (manual timeline markers)LOGI_SNAPSHOTattached to a keyboard shortcut (on-demand snapshots)LOGI_EVENT_PROBEattached to Button 6 (thumb button) for event routing checks
Logging:
- Dual logging (direct file write + unified log) to
logs/logi.log - Quiet-by-default state change logging
- LaunchAgent
logstream.shfor continuous collection
Tests executed:
- Easy-Switch event routing tests (multiple runs; success + failure cases)
- Easy-Switch cursor/macro timing tests (simple vs gesture macros)
- Sleep/Wake cycles with reproduction of
IOHIDDeviceSetReport failed - BTT restart recovery test
- BTT device log collection in both GOOD and FAILURE states
- API instrumentation with retry logic + stability checks
Established findings
-
Multiple distinct failure modes
- Easy-Switch failure mode:
HID++ error: 0x04appears when the device is switched away.- On return, API reports ready (
btnOk=true) but DPI often shows transient values (1000 -> 4000) and sometimes gets stuck at 1000. - Cursor moves slowly when DPI is stuck at 1000.
- Sleep/Wake failure mode:
IOHIDDeviceSetReport failederror appears.- Macros can fully stop working even when cursor speed is normal.
- Error can persist indefinitely until BTT is restarted.
- Combined mode:
- Sleep/Wake with
IOHIDDeviceSetReport failedplus slow cursor (DPI issues).
- Sleep/Wake with
- Easy-Switch failure mode:
-
Event routing is not always the blocker
- Event probe triggers (LOGI_EVENT_PROBE) show that button events can reach BTT before DPI stabilizes in successful recoveries.
- Therefore: "events never reach BTT during recovery" is refuted in successful cases.
-
DPI + button diversion are timing-sensitive
logitech_set_dpi()can timeout during failure states.logitech_divert_button()can timeout during unstable recovery.- Retry with exponential backoff succeeds when the device stabilizes.
- Stability checks are required before applying configuration.
-
Device enumeration can fail during failure states
- BTT "Gather Logitech Device Logs" restarts the Logitech manager.
- In a failure state (DPI=1000), the manager restart often results in:
- "No Logitech HID++ devices found"
- "Device not found" / request timeouts
- In a working state, enumeration succeeds and device details are fully captured.
-
isReadyandget_mode()are unreliable health signalsdev.isReadycan remain true even when the mouse is switched away.logitech_get_mode()often returns undefined even when working.
-
Selective button failure pattern
- Control ID mappings:
- Button 3 ->
0x0053 - Button 4 ->
0x0056 - Button 6 ->
0x00C3 - Button 7 ->
0x00C4
- Button 3 ->
- During failures, lower control IDs (
0x0053,0x0056) tend to work while higher IDs (0x00C3,0x00C4) often fail.
- Control ID mappings:
Observed timing patterns
Easy-Switch (successful recoveries)
- Device away: ~10-19s (HID++ 0x04)
- API ready: immediate upon return
- DPI recovery: ~5-10s (1000 -> 4000)
- Simple macros: can work ~0.9s after return
- Cursor speed recovery: ~9-15s after return
Easy-Switch (failed recoveries)
- API ready, but DPI stuck at 1000; cursor stays slow for minutes.
Sleep/Wake
- Failures observed after 2-4 cycles.
IOHIDDeviceSetReport failedpersists; macros stop working.- Restarting BTT clears the error within ~10-13s.
Verified hypotheses
Confirmed:
- HID++
0x04corresponds to device away in Easy-Switch. - DPI and button diversion timeouts occur during unstable recovery.
- Retry with backoff succeeds after stability.
- Enumeration can fail completely during failure states.
Refuted:
- "Events never reach BTT during recovery."
isReadyis sufficient to detect away/back transitions.
Workarounds
- Restarting BTT restores full functionality and clears persistent failure states.