Fix Back passthrough on webOS 25 - #37
Conversation
andrewfraley
left a comment
There was a problem hiding this comment.
Thanks for digging into this — the webOS 25 Back regression looks real. A few things I'd like resolved before it merges.
Conflict with current main: 412: "back" was added to BUTTONS in "Add missing buttons from LG C5 remote", which merged after this branch point. That makes WEBOS_25_BACK_CODE = 412 a duplicate of the map entry, and more importantly the new special-case continue runs before actions = button_map.get(key), so on webOS 10+ it would swallow any user-configured back action. Worth rebasing on main and reworking that block around the existing button map.
One finding that can't be inline (the line isn't in the diff): press_button() at line 259 still resolves OUTPUT_DEVICE_NAME (Builtin [2]) directly — the device this PR declares broken on webOS 25. After this change the two write paths disagree about which device to use.
The rest is inline. The blocking ones for me are the missing Builtin [1] fallback and the unguarded os.open — both can take a currently-working setup down, at startup or mid-run.
|
|
||
| OUTPUT_DEVICE_NAME = 'LGE M-RCU - Builtin [2]' # unbound codes get resent to this device in exclusive mode | ||
| WEBOS_25_OUTPUT_DEVICE_NAME = 'LGE M-RCU - Builtin [1]' | ||
| WEBOS_25_BACK_CODE = 412 |
There was a problem hiding this comment.
412 is already in BUTTONS as "back" on main (added after this branch point), so this constant duplicates it. Suggest dropping it and matching on the resolved key == "back" instead, so there's a single source of truth for the code.
| send_input_event(device, 0, 0, 0) | ||
|
|
||
|
|
||
| def send_clean_keypress(device, keycode): |
There was a problem hiding this comment.
This reopens the device on every Back press even though output_device is already open a few lines up in input_loop. There's no try/except around the call site, so one os.open failure here terminates the daemon. Reusing the existing output_device fd avoids both the churn and that failure mode.
| return int(major_version) | ||
|
|
||
|
|
||
| def get_output_device_name(): |
There was a problem hiding this comment.
The two halves of this PR look mutually redundant. Back is intercepted below and continues before ever reaching output_device, so switching the output device can't be what fixes Back — it only changes where the other relayed keys go (volume, home, arrows, OK, wheel, and anything unmapped).
Which of the two did you verify in isolation on webOS 25? If the replay path is the actual fix, this device switch is an untested behavior change for every other passthrough key. If the device switch is the fix, the replay path can go entirely — and the long-press problem goes with it.
| fcntl.ioctl(input_device, EVIOCGRAB, 1) | ||
| output_device_path = resolve_input_device_by_name(OUTPUT_DEVICE_NAME) | ||
| output_device_name = get_output_device_name() | ||
| output_device_path = resolve_input_device_by_name(output_device_name) |
There was a problem hiding this comment.
If LGE M-RCU - Builtin [1] isn't present on a given webOS 10+ TV, resolve_input_device_by_name returns None and os.open(None, os.O_WRONLY) raises TypeError here, killing startup.
The webOS version gate is a coarse proxy for device naming, so this turns a working install into a crash-on-boot for anyone whose TV only exposes Builtin [2]. Please fall back to OUTPUT_DEVICE_NAME when the lookup comes back None.
| event = input_device.read(event_size) | ||
| (tv_sec, tv_usec, event_type, code, value) = struct.unpack(input_format, event) | ||
|
|
||
| if suppress_next_sync and event_type == 0: |
There was a problem hiding this comment.
suppress_next_sync is armed only on key-down, so the SYN following each suppressed autorepeat and the one following the suppressed key-up still fall through to os.write(output_device, event) below, carrying a stale timestamp — the exact condition send_clean_keypress exists to avoid.
The flag also isn't scoped to code 412, so it can swallow a SYN belonging to an unrelated event that happens to arrive next.
| value = 0 | ||
| buttons_waiting[code] = now | ||
|
|
||
| if EXCLUSIVE_MODE and WEBOS_MAJOR_VERSION >= 10 and event_type == 1 and code == WEBOS_25_BACK_CODE: |
There was a problem hiding this comment.
Two issues with this block:
- It sits before
actions = button_map.get(key), so on webOS 10+ it preempts anybackaction the user configured. That matters now that412: "back"is inBUTTONSonmain— the button became configurable, and this would silently disable that. - Back long-press is no longer possible: key-down, every autorepeat, and key-up are all swallowed and replaced with a fixed ~80 ms synthetic tap emitted on release. Press-and-hold registers as one short tap, and even a normal press is delayed until the button comes up.
|
Note this specific comment is really me, everything else above is the Claude Code Opus 5 review skill. |
Summary
LGE M-RCU - Builtin [1]for exclusive passthrough on webOS 25.code 412) into one fresh, complete keypress.Problem
On webOS 25, relaying remote events through the existing
Builtin [2]output changes Back behavior: pressing Back inside a nested menu exits the entire app instead of closing the menu.This affected every app while Magic Mapper was running in exclusive mode. Stopping Magic Mapper restored native Back behavior.
Hardware validation
Tested on an LG C3 running webOS 25 / Rockhopper
10.3.1-3006:[2]exited the app.[1]closed only the menu while[0]was exclusively grabbed.python3 -m py_compile magic_mapper.pyalso passes.