How the OpenLogi Mock Agent Serves the IPC Contract for GUI Development Without Hardware

TLDR: openlogi-agent-mock implements the exact tarpc RPC contract that OpenLogi's GUI expects, binding to the same local socket as the real agent while generating all device data in memory, so UI developers can build and test the desktop interface with zero hardware attached.

The OpenLogi repository (AprilNEA/OpenLogi) pairs a desktop GUI with a background agent process that communicates over a tarpc RPC service defined in [crates/openlogi-ipc/src/ipc.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs). When no Logitech hardware is available, the mock agent (openlogi-agent-mock) steps in as a drop-in replacement — it binds to the exact same socket, implements every method of the Agent trait, and serves realistic snapshots from a scripted in-memory state. This article explains exactly how the mock fulfills the IPC contract so a GUI developer never has to touch a real device.

The IPC Contract in openlogi-ipc

The contract between the GUI and agent is a tarpc service defined as the Agent trait in [crates/openlogi-ipc/src/ipc.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs). It includes methods like inventory, read_dpi, read_smartshift, set_dpi, start_pairing, pair_device, next_pairing, observe, observe_action_ring, and protocol_version.

The transport layer in [crates/openlogi-ipc/src/transport.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/transport.rs) exposes bind() and connect() helpers that create a Unix-domain socket on Linux/macOS or a Windows-named-pipe on Windows. Both the real agent and the mock use these helpers, which is what makes the mock transparent to the GUI.

Binding to the Same Socket as the Real Agent

The mock is a standalone binary located at [crates/openlogi-agent/src/bin/mockagent.rs](crates/openlogi-agent/src/bin/mock_agent.rs). Its main function calls transport::bind() just like the real agent:

let listener = transport::bind()?;   // mock_agent.rs#L87

Because bind() creates the socket at the contractually agreed path, the GUI connects to that path with transport::connect() and has no way to tell — nor any reason to care — that it's talking to a mock rather than a live agent. The socket topology is identical.

A Scripted In-Memory Device Inventory

The heart of the fake hardware is a State struct that holds a fully scripted collection of devices. According to State::new, State::render_inventory, and snapshot_of in [mock_agent.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-agent/src/bin/mock_agent.rs#L71-L84), the fake inventory includes:

  • A Bolt receiver with an online mouse, an offline mouse, and a keyboard
  • A directly-attached mouse
  • A standalone Litra light

Each device carries battery levels, DPI ranges, SmartShift status, and lighting capability flags, all stored in State.settings. The snapshot is rebuilt on every RPC call, so values can change between requests — for instance, simulating a battery draining over time.

Implementing the Full Agent Trait

The mock implements every method of the Agent trait by locking the shared Arc<Mutex<State>>, mutating or reading that state, and returning the correct result type from the contract. Every method is typed exactly like the real agent's, so the GUI's serialization layer remains unchanged.

For example, set_dpi validates the route, updates the stored DPI value, and logs the action (mock_agent.rs#L51-L62). No real HID bytes are involved.

Emulating Pairing Flows Without Bluetooth Hardware

Pairing is one of the trickiest flows in a real agent because it involves Bluetooth discovery, passkey entry, and connection state. The mock scripts the entire sequence:

  1. start_pairing creates a channel and schedules a timed discovery that posts a PairingUpdate::Found event for a fake device address (CANDIDATE_ADDRESS).
  2. pair_device simulates the user entering a passkey, then adds the synthetic device to the paired list via State::pair_scripted.
  3. next_pairing long-polls the channel, returning updates as they arrive, or None after a short hold.

A tokio::time::sleep drives each stage, so the GUI sees the same real-time pacing as actual hardware communication.

Serving Observation Streams with Synthetic Diffs

observe and observe_action_ring are supposed to return when state changes. Since no hardware generates async events, the mock implements a poll-based diff: it holds the current snapshot, waits for a short interval (OBSERVE_HOLD, 20 seconds), and then compares a freshly generated snapshot with the last one. If values changed, it emits a new Generation; otherwise the unchanged state is returned.

This pattern keeps the GUI responsive and lets it react to changes exactly as it would when a real mouse’s battery level updates over the air.

Lock-Stepping the Single-Instance Guard

The mock acquires the same agent.lock file as the real agent before binding the socket. This ensures only one process — real or mock — owns the device endpoint at any time. It also makes the mock a drop-in replacement in a dev environment because the GUI, seeing the lock, behaves identically regardless of which agent holds it.

Distinguishing the Mock from a Real Agent

The mock uses the same PROTOCOL_VERSION = 26 as the real agent but returns a version string with a -mock suffix so the GUI can display or log that it's communicating with simulated hardware. This lets developers tell at a glance which backend is active.

Running the Mock for GUI Development

To run the mock and the GUI together in development:


# In a terminal – start the mock agent

cargo run -p openlogi-agent --bin openlogi-agent-mock

# In another terminal – launch the GUI against the dev socket

OPENLOGI_DEV_AGENT=0 cargo run -p openlogi-desktop

To make a manual RPC call from a test program:

use openlogi_ipc::client::AgentClient;
use openlogi_ipc::transport;
use tarpc::context::Context;

#[tokio::main]
async fn main() {
    // Connect to the same socket the mock binds to
    let transport = transport::connect().await.unwrap();
    let client = AgentClient::new(tarpc::client::Config::default(), transport);
    let ctx = Context::current();

    // Get the scripted inventory
    let inventory = client.inventory(ctx).await.unwrap();
    println!("{} devices reported", inventory.len());

    // Query the mock's protocol version
    let version = client.protocol_version(ctx).await.unwrap();
    println!("Protocol version: {}", version);
}

Key Files in the Mock

File Role
[crates/openlogi-agent/src/bin/mock_agent.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-agent/src/bin/mock_agent.rs) Implements the mock Agent service, holds the scripted State, and runs the tarpc server.
[crates/openlogi-ipc/src/ipc.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs) Defines the tarpc Agent contract, data structures (AgentSnapshot, PairingUpdate), and protocol version.
[crates/openlogi-ipc/src/transport.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-ipc/src/ipc.rs) Provides bind() and connect() helpers for the local-socket transport used by both mock and real agents.
[crates/openlogi-core/src/device.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/device.rs) Contains the core device models (DeviceInventory, PairedDevice, etc.) that the mock populates.
[crates/openlogi-core/src/config.rs](https://github.com/AprilNEA/OpenLogi/blob/master/crates/openlogi-core/src/config.rs) Holds configuration structs; the mock reads the default config to populate AgentStatus.

Summary

  • openlogi-agent-mock binds to the same socket as the real agent via transport::bind(), making it a drop-in replacement for GUI development.
  • All device data lives in an in-memory State struct and is served through the exact Agent rRPC trait, so the GUI logic stays identical. Som
  • Pairing, observation, and inventory flows are scripted with tokio::time::sleep, so the GUI experiences realistic timing and state changes without real hardware.
  • The mock uses the same agent.lock guard and protocol version, returning a -mock version suffix to distinguish itself.

Frequently Asked Questions

Does the mock agent require any physical Logitech device?

No. The mock binds to a local socket, builds an in-memory device inventory, and responds to RPC calls without invoking any HID I/O, input hooks, or Accessibility dialogs. Everything runs inside RAM.

Can the GUI tell the difference between the mock and the real agent?

Yes. While the mock uses the same PROTOCOL_VERSION (e.g., 26) as the real agent, its protocol_version RPC returns a string with a -mock suffix. This lets the GUI display the mock mode or gate testing-specific behavior.

Does the mock enforce the same single-instance rule as the real agent?

Yes. The mock acquires the same agent.lock file that the real agent does, so only one process can bind the agent socket at any given time. Starting the mock and the real agent simultaneously will fail, which prevents accidental conflicts during development.

How does the observation stream work without real events?

observe and observe_action_ring hold the current snapshot for OBSERVE_HOLD (about 20 seconds), then compare a newly generated snapshot to the last one. If the scripted state changed (e.g., a battery dropped), a new Generation is returned; otherwise the unchanged state is returned. This simulates the real agent's event-triggered behavior.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →