Skip to content

fix(ulanzi): Linux evdev access — detect inaccessible dial + one-click udev grant#3677

Merged
ten9876 merged 2 commits into
mainfrom
feat/ulanzi-dial-linux-access
Jun 20, 2026
Merged

fix(ulanzi): Linux evdev access — detect inaccessible dial + one-click udev grant#3677
ten9876 merged 2 commits into
mainfrom
feat/ulanzi-dial-linux-access

Conversation

@ten9876

@ten9876 ten9876 commented Jun 20, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes the Linux Ulanzi Dial "disconnected" failure (Closes #3599, repurposed from its original BLE-GATT premise — see that issue for why).

On Linux, BlueZ HoG bridges the dial fine and creates /dev/input/event* nodes, but they're root:input 0660. A normal AetherSDR process (not root, not in input, no uaccess ACL) can't open() them — and EvdevEncoderManager::findMatchingDevice() hit EACCES and silently continued, so the dial read as "disconnected" with no diagnostic.

What changed (Linux-specific — evdev access defect; Windows/macOS unchanged)

  • Detect a present-but-inaccessible dial: match on the sysfs device name (/sys/class/input/event*/device/name, world-readable) so the dial is recognized even when its node can't be opened. On EACCES, emit a new accessRequired(name) signal + a clear log line instead of failing silently.
  • One-click on-demand grant: the Ulanzi mapper shows a "Grant access" button on accessRequired → installs a uaccess udev rule via polkit/pkexec, reloads udev, re-scans. Gated on real detection + user consent — nothing lands on dial-less machines.
  • packaging/linux/70-ulanzi-dial.rules — canonical rule, committed as reference / distro-packager opt-in; not installed by the build.

Verification (Principle XI — demonstrated live)

On an Intel iGPU box with a BLE Ulanzi Dial:

  • Before: open(/dev/input/event25)EACCES (user not in input, no uaccess).
  • After installing the rule: logind applies user:<me>:rw automatically on connect; the dial attaches, and turning it tunes the radio — source=ulanzi-dial intent=IncrementalTune confirmed in the log.
  • The dial emits exactly the decoder's expected keycodes (rotation KEY_VOLUMEUP/DOWN; buttons KEY_PLAYPAUSE/MUTE/PREVIOUSSONG/NEXTSONG; chords Ctrl+Y/Z/V/C).

Notes

  • Builds clean on Linux. The new code is #ifdef Q_OS_LINUX-guarded, so Windows/macOS are unaffected (the accessRequired signal and grant-access UI are Linux-only).
  • The separate Windows H100 issue (ULANZI H100 DIAL NO CONNECT #3670) is a different root cause (product-string match) and is not addressed here.

🤖 Generated with Claude Code

…k udev grant

On Linux the Ulanzi Dial showed "disconnected" even when paired and presenting
input nodes. BlueZ HoG bridges the dial fine (it creates /dev/input/event*),
but those nodes are root:input 0660 — a normal AetherSDR process (not root, not
in 'input', no uaccess ACL) can't open them. findMatchingDevice() hit EACCES
and silently continued, so the dial read as disconnected with no diagnostic.

Fix (Linux-specific — an evdev access defect; Windows/macOS HID paths unchanged):

- EvdevEncoderManager matches on the sysfs device name
  (/sys/class/input/event*/device/name, world-readable) so a present-but-
  inaccessible dial is still recognized. On EACCES it emits a new
  accessRequired(name) signal and logs a clear warning instead of failing
  silently.
- UlanziDialMapperDialog shows a "Grant access" button on accessRequired that
  installs a uaccess udev rule via polkit/pkexec, reloads udev, and re-scans.
  Gated on real device detection + user consent — nothing is installed on
  machines without the dial.
- packaging/linux/70-ulanzi-dial.rules: the canonical rule, committed as a
  reference / distro-packager opt-in; not installed by the build.

Verified live on Intel iGPU + BLE Ulanzi Dial: detected -> permission granted
-> rotation tunes the radio. The original BLE-GATT-backend premise of #3599 is
no longer reproducible (BlueZ HoG now bridges cleanly, likely a kernel/BlueZ
update), so the real blocker was access, not transport.

Closes #3599.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876 ten9876 requested a review from a team as a code owner June 20, 2026 04:19

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ten9876 — this is a well-scoped, well-documented fix. The root-cause analysis (EACCES on the evdev node being silently continued) is correct, and the approach of matching on the world-readable sysfs name so a present-but-unopenable dial is still recognized is the right call. The #ifdef Q_OS_LINUX guarding is clean and keeps Windows/macOS untouched. A few notes, all minor.

1. QProcess start failure isn't handled (minor bug)
onGrantAccessClicked() only connects QProcess::finished. If pkexec is found but the process fails to start (e.g. exec error), Qt emits errorOccurred(FailedToStart) and finished is never emitted — so the button stays disabled showing "Authorizing…" with no way to recover. Suggest also connecting errorOccurred to reset the button/status:

connect(proc, &QProcess::errorOccurred, this, [this, proc](QProcess::ProcessError) {
    if (proc->state() == QProcess::NotRunning) {
        proc->deleteLater();
        if (m_grantAccessBtn) {
            m_grantAccessBtn->setEnabled(true);
            m_grantAccessBtn->setText(tr("Grant access"));
        }
        if (m_statusLabel) m_statusLabel->setText(tr("Access install failed"));
    }
});

2. Wrong issue number in the udev rule comment (doc nit, but propagated)
Both packaging/linux/70-ulanzi-dial.rules and the inline kRule header reference #3670. Per your own PR description this fix Closes #3599, and #3670 is the Windows H100 issue that is explicitly not addressed here. Since the rule is committed as canonical reference, it'd be good to correct it to #3599 to avoid pointing future readers at the wrong root cause.

Things I checked that are fine:

  • errno is read immediately after ::open() with no intervening libc call, so the EACCES/EPERM check is reliable.
  • The rule is passed as an argv element ($1) to sh -c, not interpolated into the script string — no shell-injection surface. Good.
  • m_accessRequiredEmitted de-dup logic is correct (reset on a successful match).
  • lcDevices is declared in LogManager.h, which the dialog now includes; sysfs-name match (Ulanzi Dial Keyboard) is consistent with supportedNames().

Nothing here is blocking from my side — the errorOccurred gap is the one I'd most want addressed. Nice work.


🤖 aethersdr-agent · cost: $1.2580 · model: claude-opus-4-8

… review)

Two review fixes:

1. onGrantAccessClicked() only connected QProcess::finished. If pkexec is found
   but fails to *start*, Qt emits errorOccurred and never emits finished, so the
   button would stay stuck on "Authorizing…". Connect errorOccurred to recover
   the button/status when the process never ran.

2. The udev rule header (both packaging/linux/70-ulanzi-dial.rules and the
   inline kRule) referenced #3670, but this fix Closes #3599#3670 is the
   separate Windows H100 issue. Corrected to #3599.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@ten9876 ten9876 merged commit 29c3ad5 into main Jun 20, 2026
6 checks passed
@ten9876 ten9876 deleted the feat/ulanzi-dial-linux-access branch June 20, 2026 13:55
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.

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

1 participant