The daemon follows an actor-based architecture where each connected device runs as an independent async task communicating through channels. This design:
- Isolates device failures (one device crashing doesn't affect others)
- Simplifies concurrency (no shared mutable state between devices)
- Enables natural hotplug handling (spawn/drop actors on connect/disconnect)
- Maps well to async Rust patterns
┌─────────────────────────────────────────────────────────────────────┐
│ logiops-daemon │
├─────────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Signal │ │ Config │ │ D-Bus Service │ │
│ │ Handler │───▶│ Manager │◀───│ (zbus) │ │
│ │ (SIGTERM, │ │ (SIGHUP │ │ org.logiops.Device1 │ │
│ │ SIGINT) │ │ reload) │ └──────────────────────┘ │
│ └──────────────┘ └──────────────┘ ▲ │
│ │ │ │ │
│ ▼ ▼ │ │
│ ┌─────────────────────────────────────────────────┴─────────────┐ │
│ │ Device Registry │ │
│ │ (Arc<RwLock<HashMap<String, DeviceHandle>>>) │ │
│ └───────────────────────────┬───────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────┼─────────────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │ Device Actor │ │ Device Actor │ ... │ udev │ │
│ │ (per device) │ │ (per device) │ │ Monitor │ │
│ │ │ │ │ │ (tokio-udev)│ │
│ │ ┌──────────┐ │ │ ┌──────────┐ │ └─────────────┘ │
│ │ │ HID++ │ │ │ │ HID++ │ │ │
│ │ │ Channel │ │ │ │ Channel │ │ │
│ │ └────┬─────┘ │ │ └────┬─────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌────▼─────┐ │ │ ┌────▼─────┐ │ │
│ │ │ uinput │ │ │ │ uinput │ │ │
│ │ │ (evdev) │ │ │ │ (evdev) │ │ │
│ │ └──────────┘ │ │ └──────────┘ │ │
│ └──────────────┘ └──────────────┘ │
│ │ │ │
└─────────┼───────────────────┼──────────────────────────────────────┘
▼ ▼
/dev/hidraw0 /dev/hidraw1 /dev/input/eventX
- Signal handling (SIGTERM, SIGINT for shutdown; SIGHUP for config reload)
- Systemd notification (sd-notify ready/stopping)
- Task orchestration and shutdown coordination
- Loads configuration from multiple sources (system, user, env)
- Watches for SIGHUP to trigger reload
- Distributes config updates to device actors
- Central registry of active devices
- Thread-safe access via
Arc<RwLock<HashMap>> - Provides handles for D-Bus and CLI queries
- Watches
/dev/hidraw*for Logitech devices (VID 046d) - Emits hotplug events (DeviceAdded, DeviceRemoved)
- Runs as dedicated async task
Each connected device runs as an independent actor:
- Owns its HID++ channel
- Processes commands from registry (via mpsc channel)
- Polls for button events when buttons are configured (every 10ms)
- Tracks pressed button state for generating press/release events
- Handles device events (button presses, battery updates)
- Cleans up resources on drop
Shared across all device actors (via Arc<Mutex>):
- Owns the
LogiopsVirtualDevice(uinput) - Executes configured actions on button events
- Tracks DPI cycle index per CID
- Tracks held keys for proper release
- Supports: Keypress, CycleDpi, ToggleSmartShift, None
- Exposes
org.logiops.Manager1interface - Lists devices, queries battery, sets DPI
- Emits signals for device add/remove events
1. udev detects /dev/hidrawN with VID=046d
2. udev monitor emits DeviceAdded event
3. Main loop receives event, spawns new DeviceActor
4. DeviceActor:
a. Opens HID device
b. Queries device info via HID++
c. Enumerates features
d. Creates uinput device (if remapping enabled)
e. Applies configuration
f. Enters event loop
5. Registry stores DeviceHandle
6. D-Bus emits DeviceAdded signal
1. Button configured for divert in config.toml
2. DeviceActor sets CID reporting to divert via HID++ 0x1B04
3. User presses button on mouse
4. HID++ notification (function 0) arrives on /dev/hidraw
5. DeviceActor polls and parses button event:
- Compares current pressed CIDs with previous state
- Generates Press/Release events for changes
6. ActionExecutor processes button based on action type:
- Keypress: keys_down() on press, keys_up() on release
- CycleDpi: Advance DPI index, set new DPI via HID++
- ToggleSmartShift: Toggle active state via HID++
- None: Ignore (button disabled)
7. LogiopsVirtualDevice emits key events via /dev/uinput
8. Event appears at /dev/input/eventX
9. Desktop environment receives remapped key
Button Event Parsing:
HID++ Notification: [0x11, device_idx, feature_idx, 0x00, CID_hi, CID_lo, 0, 0, ...]
- Function ID 0 = diverted button event
- CIDs listed = currently pressed buttons
- Empty CID list = all buttons released
- State tracking: Compare current vs previous to detect press/release
1. User sends SIGHUP to daemon
2. Signal handler sets reload flag
3. Config manager re-reads files
4. For each device:
a. Send ApplyProfile command via channel
b. Actor updates settings
5. Log confirmation
- Connection lost: Actor detects read error, logs warning, exits cleanly
- Invalid response: Log error, continue operation (device may recover)
- Feature unsupported: Skip configuration, log info
- udev monitor fails: Log error, attempt restart with backoff
- D-Bus connection lost: Log error, attempt reconnect
- Config invalid: Keep previous config, log error
- Graceful: CancellationToken propagates to all actors
- Timeout: Force exit after 10 seconds if tasks don't complete
- Cleanup: Each actor drops its uinput device and closes HID handle
| Resource | Protection | Access Pattern |
|---|---|---|
| Device Registry | Arc<RwLock<>> |
Read-heavy, occasional writes |
| HID Device | Actor ownership | Single-threaded per device |
| Configuration | Arc<RwLock<>> |
Read-mostly, rare reloads |
| D-Bus Connection | Owned by zbus | Thread-safe internally |
root_token (main)
├── udev_token (child) → udev monitor task
├── dbus_token (child) → D-Bus service task
└── device_tokens (children) → per-device actors
Cancelling the root token propagates to all children for coordinated shutdown.
let tracker = TaskTracker::new();
// Spawn tasks
tracker.spawn(device_actor.run());
tracker.spawn(udev_monitor.run());
// Graceful shutdown
tracker.close();
tracker.wait().await; // Waits for all tasks