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

> Learn how the OpenLogi mock agent serves the IPC contract for GUI development without hardware. Build and test desktop interfaces using in-memory data with zero hardware.

- Repository: [Xuan Zhang/OpenLogi](https://github.com/AprilNEA/OpenLogi)
- Tags: how-to-guide
- Published: 2026-08-23

---

**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/main/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/main/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/main/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`](https://github.com/AprilNEA/OpenLogi/blob/main/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:

```rust
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/main/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:

```bash

# 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:

```rust
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/main/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/main/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/main/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/main/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/main/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.