fix: 🐛 Stop the GUI hanging on close and reconnect the BLE keyboard when its hidraw node changes - #7
Open
sammyapoex wants to merge 2 commits into
Open
Conversation
updateLayer had its `await asyncio.sleep(0.1)` inside the try block, so any exception skipped the only yield point in the loop. Once the window starts closing, every iteration raises (the Image widget is gone) and the coroutine spins without ever awaiting. That starves the asyncio event loop, so async_run() can never finish, the window stops answering the compositor, and the app sits there until it is force-quit. The Kivy logs show it plainly: "Window: exiting mainloop and closing." followed by hundreds of "--- Logging error ---" lines and no exit. - Move the sleep out of the try so an error can never skip the yield. - Cancel the task in on_stop so the loop actually ends on shutdown. - Bounds-check the layer index instead of indexing config.layers blind, so a layer with no configured image logs and is ignored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A ZMK keyboard that drops its BLE link comes back on a *different*
/dev/hidraw node. Captured here:
09:02:01 hidraw5 (Kibard) disappears
09:02:06 Kibard returns as hidraw7
notify_changes only looked for the device again from inside its
HIDException handler, and did so with
self.hid = self.find_device() or self.hid
Because the keyboard is absent during the gap, find_device() returns
None and the `or` puts the dead handle straight back. Nothing ever
enumerates again, and reads on that handle are quiet rather than fatal,
so the app sits there polling a device that is gone: window perfectly
responsive, no error printed, CPU normal, layer image frozen for good.
- Track the hidraw node we opened and re-open when it disappears; a
stat() per poll is cheap enough for the GUI loop, unlike enumerate().
- Never keep a handle we failed to re-find, and close the old one so
repeated blips do not leak descriptors.
- Throttle reconnect attempts to 1s so hid.enumerate() cannot stall the
event loop while the keyboard is away.
- Reset current_layer on reconnect, so a keyboard returning on the layer
we already display still redraws instead of comparing equal.
- Drop the unbounded tenacity @Retry: its wait_exponential called
blocking time.sleep() on the Kivy event loop, freezing the window for
1, 2, 4, 8, 10... seconds forever. The GUI already re-polls every
100ms, so returning None is both simpler and non-blocking.
Only the BLE path is changed; keyboard_hid.py (USB raw HID) has a
different report shape and is untested here.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Owner
|
Thanks for you contribution @sammyapoex ! I will go through the changes and get back to you. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note: This pr was generated with Claude. I don't have any experience with this code base, so these changes should be reviewed thoroughly. But I can confirm that it fixed the issues with the app freezing that I was having.
Two independent hangs I ran into while using the app on Linux with a ZMK keyboard over Bluetooth.
1. GUI hangs on close (
libs/gui.py)updateLayerhad itsawait asyncio.sleep(0.1)inside thetry, so any exception skipped the loop's only yield point. Once the window starts closing every iteration raises (theImagewidget is gone) and the coroutine spins without ever awaiting, starving the asyncio event loop —async_run()never finishes, the window stops answering the compositor, and the app has to be force-quit. The Kivy log showsWindow: exiting mainloop and closing.followed by hundreds of--- Logging error ---lines and no exit.sleepout of thetryso an error can never skip the yield.on_stopso the loop actually ends on shutdown.config.layersblind, so a layer with no configured image logs and is ignored.2. Frozen layer image after a BLE drop (
libs/keyboard_ble_hid.py)A keyboard that drops its BLE link comes back on a different
/dev/hidrawnode:notify_changesonly looked for the device again from inside itsHIDExceptionhandler, and did so withself.hid = self.find_device() or self.hid. The keyboard is absent during the gap, sofind_device()returnsNoneand theorputs the dead handle straight back. Nothing ever enumerates again, and reads on that handle are quiet rather than fatal, so the app polls a device that is gone: window responsive, no error printed, CPU normal, layer image frozen for good.stat()per poll is cheap enough for the GUI loop, unlikeenumerate().hid.enumerate()can't stall the event loop while the keyboard is away.current_layeron reconnect, so a keyboard returning on the layer already displayed still redraws instead of comparing equal.tenacity @retry: itswait_exponentialcalled blockingtime.sleep()on the Kivy event loop, freezing the window for 1, 2, 4, 8, 10... seconds forever. The GUI already re-polls every 100 ms, so returningNoneis simpler and non-blocking.Only the BLE path is changed —
libs/keyboard_hid.py(USB raw HID) has a different report shape and is untested here, so it is left alone.tenacityis still used bylibs/client.pyandlibs/keyboard_hid.py, so it stays a dependency.🤖 Generated with Claude Code