Skip to content

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
maatthc:masterfrom
sammyapoex:fix/gui-hang-on-close
Open

fix: 🐛 Stop the GUI hanging on close and reconnect the BLE keyboard when its hidraw node changes#7
sammyapoex wants to merge 2 commits into
maatthc:masterfrom
sammyapoex:fix/gui-hang-on-close

Conversation

@sammyapoex

@sammyapoex sammyapoex commented Sep 3, 2026

Copy link
Copy Markdown

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)

updateLayer had its await asyncio.sleep(0.1) inside the try, so any exception skipped the loop's only yield point. Once the window starts closing every iteration raises (the Image widget 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 shows 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.

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/hidraw node:

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. The keyboard is absent during the gap, so 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 polls a device that is gone: window 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 don't leak descriptors.
  • Throttle reconnect attempts to 1s so hid.enumerate() can't stall the event loop while the keyboard is away.
  • Reset current_layer on reconnect, so a keyboard returning on the layer already displayed 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 100 ms, so returning None is 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. tenacity is still used by libs/client.py and libs/keyboard_hid.py, so it stays a dependency.

🤖 Generated with Claude Code

Sammy Henningsson and others added 2 commits September 2, 2026 08:35
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>
@maatthc

maatthc commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Thanks for you contribution @sammyapoex ! I will go through the changes and get back to you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants