Skip to content

Linux: Ulanzi Dial 'disconnected' — evdev access (udev/uaccess) + enable-toggle gap #3599

Description

@ten9876

Repurposed 2026-06-20. This issue originally proposed a native BLE-GATT
dial backend to work around a BlueZ HID-over-GATT (HoG) attach failure. That
failure is no longer reproducible — on current hardware BlueZ bridges the
Ulanzi Dial cleanly (it creates /dev/input/event* nodes with a valid
keyboard descriptor), most likely fixed by a BlueZ/kernel update since the
original report (2026-06-14). With HoG working, the real remaining blocker
turned out to be evdev access + the disabled enable-toggle, which is what
this issue now tracks. The original BLE-GATT diagnosis is preserved at the
bottom as a contingency record.

Problem

On Linux the Ulanzi Dial shows "disconnected" in the mapper even though the
device is paired, connected, and presenting input nodes. Two compounding causes,
both verified on hardware:

  1. The feature is off by default. UlanziDialEnabled defaults to False
    (intentionally, to avoid the macOS Input-Monitoring prompt — privacy(hid): UlanziDialMacOSManager unconditionally requests Input Monitoring permission on every macOS launch #3257), so the
    Linux backend never even scans unless the user ticks Settings → Serial →
    USB Control Surfaces → "Enable Ulanzi Dial"
    .
  2. The evdev node isn't accessible. BlueZ HoG creates the dial's
    /dev/input/event* nodes as root:input 0660. A normal AetherSDR process
    (not root, not in the input group, no uaccess ACL) cannot open()
    them
    . EvdevEncoderManager::findMatchingDevice() hit EACCES and
    silently continued — so even with the feature enabled and the name
    matching exactly, the dial read as disconnected with no diagnostic.

Verified on hardware

  • BlueZ HoG bridges fine: the dial enumerates as Ulanzi Dial Keyboard
    (event25) + Mouse + two Ulanzi Dial nodes (HID 0005:FFF1:0082).
  • It emits exactly the keycodes the decoder expects: rotation =
    KEY_VOLUMEUP/KEY_VOLUMEDOWN, buttons = KEY_PLAYPAUSE/MUTE/
    PREVIOUSSONG/NEXTSONG, chords = Ctrl+Y/Z/V/C.
  • Before the fix: open("/dev/input/event25")EACCES (user not in input,
    no uaccess). After installing a uaccess udev rule: logind applies a
    user:<me>:rw ACL automatically on connect, open() succeeds, and turning
    the dial tunes the radio (source=ulanzi-dial intent=IncrementalTune).

Fix

Linux-specific (this is a Linux-specific access defect — the cross-platform
exception; Windows/macOS HID paths unchanged):

  • Detect a present-but-inaccessible dial. EvdevEncoderManager now reads the
    device name from sysfs (/sys/class/input/event*/device/name, always
    world-readable), so it recognizes the dial even when it can't open the node.
    On EACCES it emits a new accessRequired(name) signal and logs a clear
    warning instead of failing silently.
  • One-click, on-demand access grant. When accessRequired fires, the Ulanzi
    Dial mapper shows a "Grant access" button that installs a uaccess udev
    rule via polkit/pkexec (KDE/GNOME auth prompt), reloads udev, and
    re-scans. Gated on actual device detection + user consent — nothing is
    installed on machines that don't have the dial.
  • packaging/linux/70-ulanzi-dial.rules — the canonical rule, committed as a
    reference and for distro packagers who prefer to ship it unconditionally; it
    is not installed by the build.

Constitution

  • Cross-platform principle — Linux-only code is justified (Linux-specific
    evdev access defect); Windows/macOS unchanged.
  • Principle XI (demonstrate) — verified live: dial detected → permission
    granted → rotation tunes the radio.

Related

  • ULANZI H100 DIAL NO CONNECT #3670 — Windows H100 "no connect" is a different root cause (the Windows
    matcher hardcodes the product string "Ulanzi Dial"; the H100 may report a
    different name). Separate matcher-broadening task, still awaiting the
    reporter's exact device string.

Original BLE-GATT-backend diagnosis (2026-06-14) — kept as a contingency record

The original premise was that BlueZ HoG failed to attach (input-hog profile accept failed), so no /dev/input/event* node was created and a native
BLE-GATT backend was needed to read the dial directly over org.bluez GATT.
That HoG-attach failure is not reproducible on current hardware. If it recurs on
other setups (older BlueZ, different dial firmware), the BLE-GATT-backend
approach below is the fallback — the HID data is fully reachable over GATT.

Captured HID Report Map (RID 1 = Keyboard, RID 2 = Consumer, RID 3 = Mouse,
RID 251/252 = vendor 64-byte reports):

05 01 09 06 A1 01 85 01 05 07 19 E0 29 E7 15 00 25 01 75 01 95 08 81 02 95 01 75 08 81 03
95 05 75 01 05 08 19 01 29 05 91 02 95 01 75 03 91 03 95 06 75 08 15 00 25 FF 05 07 19 00
29 FF 81 00 C0 05 01 09 02 A1 01 85 03 09 01 A1 00 05 09 19 01 29 05 15 00 25 01 95 05 75
01 81 02 95 01 75 03 81 03 05 01 09 30 09 31 09 38 15 80 25 7F 75 08 95 03 81 06 05 0C 0A
38 02 75 08 95 01 81 06 C0 C0 05 0C 09 01 A1 01 85 02 75 10 95 01 15 01 26 FF 03 19 01 2A
FF 03 81 60 C0 06 F1 FF 09 01 A1 01 85 FC 09 02 15 00 26 FF 00 75 08 95 40 81 02 C0 06 FD
FF 09 01 A1 01 85 FB 09 02 15 00 26 FF 00 75 08 95 40 81 02 09 03 15 00 26 FF 00 75 08 95
40 91 02 C0

Proposed approach: BleHidDialManager (Linux only) via QtDBus → org.bluez
GATT; StartNotify on the report characteristics and decode per the descriptor,
emitting the same encoder/button signals as EvdevEncoderManager; aggregate so
the evdev backend is preferred and BLE-GATT is the fallback when HoG fails.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementImprovement to existing featureexternal devicesFlexControl, MIDI, serial port, USB peripheralslinuxmaintainer-reviewRequires maintainer review before any action is takenpriority: lowLow priority

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions