How openlogi-agent-mock Simulates Device Inventory for Development and Testing
The openlogi-agent-mock binary provides a hardware-free, scripted implementation of the tarpc Agent service that builds an in-memory inventory mimicking real Logitech devices, enabling developers to run the OpenLogi GUI without any physical hardware attached.
The openlogi-agent-mock is a critical development tool in the OpenLogi ecosystem (AprilNEA/OpenLogi) that eliminates hardware dependencies for testing and UI development. By simulating device inventory through a carefully crafted state machine, it allows developers to iterate on the desktop client using realistic, dynamic data that behaves like actual Logitech peripherals.
Creating the Scripted Device State
The mock agent initializes its entire inventory through State::new() in crates/openlogi-agent/src/bin/mock_agent.rs. This function pre-populates a HashMap<u8, DeviceSettings> that defines capabilities for multiple device types.
The initial state includes:
- A primary mouse with DPI and SmartShift settings
- An offline mouse to test disconnected-state handling
- A keyboard with configurable lighting
- A directly-attached mouse for testing non-Bolt connections
Each entry maps a slot number (u8) to DeviceSettings structs describing the device's configurable parameters. The function also tracks next_slot for scripted pairing operations.
// From the OpenLogi source: State::new (lines 88-138)
// Pre-populates device capabilities without any hardware queries
let state = State::new()?;
Rendering Dynamic Inventory on Demand
The State::render_inventory() method (lines 449-454) reconstructs the complete inventory on every RPC call. This ensures the GUI always receives fresh, potentially mutated state.
The rendering process combines multiple inventory sources:
- Bolt receiver inventory — generated by
bolt_inventory()with simulated battery drain - Scripted pairing additions — devices added through mock pairing flow
- Direct-attached inventory — standalone mouse reporting
// Conceptual flow of render_inventory
let mut inventory = Vec::new();
inventory.push(self.bolt_inventory()); // Receiver + paired devices
inventory.extend(self.paired_scripted()); // Newly paired devices
inventory.push(self.direct_inventory()); // Direct USB device
Bolt Receiver Simulation with Battery Dynamics
The bolt_inventory() function (lines 559-650) constructs the most complex inventory component. It returns a DeviceInventory containing:
| Component | Description |
|---|---|
| Online mouse | Active device with saw-tooth battery curve |
| Offline mouse | Disconnected device (battery: None) |
| Keyboard | With lighting: true capability flag |
The battery simulation deserves special attention. The draining_battery() helper (lines 443-457) computes a saw-tooth curve that decreases approximately 1% per minute, wrapping back to full charge after depletion. This creates visible UI activity without actual power management.
// Battery level changes on every poll call
battery_percentage: draining_battery(start_instant),
Standalone Lighting Device
Beyond traditional input devices, the mock implements standalone_light() (lines 495-520) representing a Litra Glow video light. This device exposes:
- Power state (on/off)
- Brightness level
- Color temperature
The GUI can control these via set_light RPC calls, with the mock immediately updating its internal state and reflecting changes in subsequent inventory renders.
Simulating Foreground Application Context
Real Logitech software adapts settings per application. The mock replicates this through State::foreground() (lines 421-444), which cycles through a hard-coded list every 10 seconds:
com.apple.Safaricom.microsoft.VSCodecom.apple.Finder
This deterministic rotation allows developers to verify the GUI's per-app profile rendering without a window server or actual process monitoring.
// Foreground switching enables testing app-specific DPI profiles
let app = state.foreground(); // Changes every 10s automatically
RPC Exposure and Pairing Flow
The Agent trait implementation forwards inventory() calls directly to State::render_inventory() (lines 91-93), ensuring the GUI receives identical data structures as from a real agent.
The scripted pairing flow demonstrates full state transition:
start_pairing() → discovery delay → passkey exchange → pair_device()
The mock uses tokio::time::sleep constants to simulate realistic timing:
// Fake device discovery (lines 120-150)
let candidate = CANDIDATE_ADDRESS; // Hard-coded MAC-like address
tokio::time::sleep(DISCOVERY_DELAY).await; // Simulated radio discovery
// Passkey verification steps...
state.pair_scripted(new_device); // Add to inventory (lines 669-688)
Running the Mock Agent
Start the mock server:
cargo run -p openlogi-agent --bin openlogi-agent-mock
Then launch the GUI against the development socket:
OPENLOGI_DEV_AGENT=0 cargo run -p openlogi-desktop
Programmatic interaction follows the same patterns as live hardware:
// Example DPI change via RPC
let route = DeviceRoute::Bolt {
receiver_uid: RECEIVER_UID,
slot: MOUSE_SLOT,
};
agent.set_dpi(context, route, Dpi::new(2000)).await?;
Key Source Files
| File | Purpose |
|---|---|
crates/openlogi-agent/src/bin/mock_agent.rs |
Core mock implementation, inventory construction, RPC handlers |
crates/openlogi-core/src/device.rs |
DeviceInventory, PairedDevice, BatteryInfo definitions |
crates/openlogi-ipc/src/transport.rs |
Shared IPC transport for real and mock agents |
Summary
State::new()initializes a scriptedHashMapof device capabilities without hardware queriesrender_inventory()dynamically assembles fresh inventory on every RPC call, combining Bolt receiver, scripted pairings, and direct-attached devicesdraining_battery()provides a saw-tooth battery curve (≈1%/minute) for live UI updates- Foreground cycling (10s intervals) enables per-application profile testing without window server integration
- Scripted pairing simulates complete discovery → passkey → device addition flow using
tokio::timedelays - The mock exposes identical RPC interfaces to the real agent, making it interchangeable for GUI development and CI testing
Frequently Asked Questions
Does the mock agent require any Logitech hardware to function?
No. The openlogi-agent-mock operates entirely in software. It never opens USB or HID interfaces; all device data originates from the scripted State struct. This design allows CI pipelines and developers without compatible hardware to build and test the full OpenLogi stack.
How does the battery simulation work in the mock agent?
The draining_battery() function computes a saw-tooth based on elapsed time since mock start. Battery percentage decreases linearly (≈1% per minute), then wraps to 100% upon reaching zero. This recomputation happens fresh on each render_inventory() call, creating the appearance of live power monitoring without system power APIs.
Can the mock simulate device pairing and removal?
Yes. The start_pairing RPC initiates a scripted flow: artificial discovery delay, fake candidate generation (CANDIDATE_ADDRESS), passkey exchange, and final device addition via pair_scripted(). The paired device immediately appears in subsequent inventory renders at the next_slot position. Simulated removal occurs by setting device presence to false in the underlying state.
What device types does the mock inventory include?
The default State::new() populates four categories: online Bolt mouse (with DPI/SmartShift), offline Bolt mouse, keyboard with lighting support, and a directly-attached USB mouse. Additionally, standalone_light() provides a Litra Glow lighting device. Developers can extend this by modifying the mock source or adding runtime pair_scripted calls.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →