Skip to content

Latest commit

 

History

History
221 lines (174 loc) · 9.54 KB

File metadata and controls

221 lines (174 loc) · 9.54 KB

Architecture

Overview

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

System Diagram

┌─────────────────────────────────────────────────────────────────────┐
│                         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

Component Responsibilities

Main Process

  • Signal handling (SIGTERM, SIGINT for shutdown; SIGHUP for config reload)
  • Systemd notification (sd-notify ready/stopping)
  • Task orchestration and shutdown coordination

Config Manager

  • Loads configuration from multiple sources (system, user, env)
  • Watches for SIGHUP to trigger reload
  • Distributes config updates to device actors

Device Registry

  • Central registry of active devices
  • Thread-safe access via Arc<RwLock<HashMap>>
  • Provides handles for D-Bus and CLI queries

udev Monitor

  • Watches /dev/hidraw* for Logitech devices (VID 046d)
  • Emits hotplug events (DeviceAdded, DeviceRemoved)
  • Runs as dedicated async task

Device Actor

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

ActionExecutor

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

D-Bus Service

  • Exposes org.logiops.Manager1 interface
  • Lists devices, queries battery, sets DPI
  • Emits signals for device add/remove events

Data Flow

Device Connection Flow

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

Button Press Flow (Input Remapping)

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

Configuration Reload Flow

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

Error Handling Strategy

Device Errors

  • 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

System Errors

  • udev monitor fails: Log error, attempt restart with backoff
  • D-Bus connection lost: Log error, attempt reconnect
  • Config invalid: Keep previous config, log error

Shutdown

  • 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

Thread Safety Model

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

Concurrency Patterns

CancellationToken Hierarchy

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.

TaskTracker Usage

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