fix(ulanzi): Linux evdev access — detect inaccessible dial + one-click udev grant#3677
Conversation
…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>
There was a problem hiding this comment.
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:
errnois read immediately after::open()with no intervening libc call, so theEACCES/EPERMcheck is reliable.- The rule is passed as an argv element (
$1) tosh -c, not interpolated into the script string — no shell-injection surface. Good. m_accessRequiredEmittedde-dup logic is correct (reset on a successful match).lcDevicesis declared inLogManager.h, which the dialog now includes; sysfs-name match (Ulanzi Dial Keyboard) is consistent withsupportedNames().
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>
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'reroot:input 0660. A normal AetherSDR process (not root, not ininput, nouaccessACL) can'topen()them — andEvdevEncoderManager::findMatchingDevice()hitEACCESand silentlycontinued, so the dial read as "disconnected" with no diagnostic.What changed (Linux-specific — evdev access defect; Windows/macOS unchanged)
/sys/class/input/event*/device/name, world-readable) so the dial is recognized even when its node can't be opened. OnEACCES, emit a newaccessRequired(name)signal + a clear log line instead of failing silently.accessRequired→ installs auaccessudev 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:
open(/dev/input/event25)→EACCES(user not ininput, nouaccess).user:<me>:rwautomatically on connect; the dial attaches, and turning it tunes the radio —source=ulanzi-dial intent=IncrementalTuneconfirmed in the log.KEY_VOLUMEUP/DOWN; buttonsKEY_PLAYPAUSE/MUTE/PREVIOUSSONG/NEXTSONG; chordsCtrl+Y/Z/V/C).Notes
#ifdef Q_OS_LINUX-guarded, so Windows/macOS are unaffected (theaccessRequiredsignal and grant-access UI are Linux-only).🤖 Generated with Claude Code