How OpenLogi GUI and Overlay Communicate with the Agent: IPC Architecture Explained
Both the OpenLogi desktop GUI and the Actions-Ring overlay communicate with the hardware-isolated agent via a cross-platform IPC transport using tarpc RPCs over local sockets, with each client declaring its role through a versioned handshake protocol.
The OpenLogi project (AprilNEA/OpenLogi) isolates all hardware access within the openlogi-agent process. Both the desktop application (openlogi-desktop) and the overlay interface (openlogi-overlay) reside in separate processes and rely on a shared IPC crate (openlogi-ipc) to exchange structured messages with the agent. This architecture ensures consistent device management regardless of which frontend initiates the request.
The IPC Transport Layer
All communication traverses a cross-platform local socket implemented in crates/openlogi-ipc/src/transport.rs. On macOS and Linux, this uses a Unix-domain socket; on Windows, it uses a named pipe. The transport layer wraps this stream with length-delimited framing and bincode serialization to carry tarpc RPC messages between clients and the agent.
The openlogi-ipc crate abstracts platform differences, presenting a uniform client::connect() API that both the GUI and overlay consume. This design ensures that connection establishment and stream management remain consistent across operating systems.
The Connection Handshake
Before issuing device commands, every client must complete a two-step handshake defined in the tarpc service contract. According to crates/openlogi-ipc/src/ipc.rs, the client first invokes protocol_version() to verify wire compatibility. The current PROTOCOL_VERSION constant is set to 29; a mismatch causes the GUI to restart the agent or the overlay to exit immediately.
After version validation, the client declares its identity by calling declare_client() with a ClientKind enum:
ClientKind::Gui– Indicates the desktop settings application.ClientKind::Overlay– Indicates the Actions-Ring heads-up display.
This declaration occurs in crates/openlogi-desktop/src/services/ipc.rs for the GUI and in crates/openlogi-overlay/src/agent.rs for the overlay. The agent uses this information to arm specific subsystems or remain dormant when only the overlay is active.
GUI-to-Agent Communication
The GUI spawns a dedicated thread running a Tokio runtime to manage its AgentClient lifecycle. In crates/openlogi-desktop/src/services/ipc.rs, the observe_loop function establishes the connection via openlogi_ipc::client::connect(), then enters a blocking loop calling observe().
This long-polling approach allows the agent to push state changes (GuiUpdate::Snapshot) only when hardware state mutates, rather than wasting CPU on constant polling. When the GUI issues device commands—such as setting DPI, adjusting lighting, or triggering pairing—it sends fire-and-forget RPCs through the same AgentClient instance. Results return asynchronously via an internal mpsc channel back to the GPUI main loop.
// GUI: Spawn the IPC client thread
let ipc = openlogi_desktop::services::ipc::spawn();
// Inside the client thread: connect and declare the GUI role
let connection = openlogi_ipc::client::connect().await?;
connection.client
.declare_client(context::current(), ClientKind::Gui)
.await?;
// GUI: Request a state update (blocking until generation changes)
let ctx = context::current();
let obs = client.observe(ctx, last_generation).await?;
Overlay-to-Agent Communication
The overlay follows a similar connection pattern but optimizes for low-latency input handling. In crates/openlogi-overlay/src/agent.rs, the spawn_ipc() function initiates the connection and declares ClientKind::Overlay. It then splits execution into two concurrent tasks:
poll_invocations– Callsobserve_action_ring()to block until the Actions-Ring state changes, pushing updates onto anmpsc::UnboundedReceiverfor the UI thread.send_commands– Listens for UI events (Hover,Activate,Cancel) and forwards them viaaction_ring_hover(),action_ring_activate(), andaction_ring_cancel(). This task handles command coalescing and retry logic to ensure that rapid user inputs do not overwhelm the agent.
Unlike the GUI's general-purpose observe() method, the overlay uses observe_action_ring(), which returns a slimmer payload focused exclusively on ring invocation state.
// Overlay: Start the IPC runtime and declare the overlay role
let client = openlogi_ipc::client::connect().await?;
client.client
.declare_client(context::current(), ClientKind::Overlay)
.await?;
// Overlay: Watch the ring invocation
let obs = client.observe_action_ring(ctx, last_generation).await?;
if let Some(invocation) = obs.invocation {
// Forward invocation to the overlay UI
}
The RPC Protocol Contract
All method signatures reside in crates/openlogi-ipc/src/ipc.rs within the Agent tarpc service definition. This centralized contract ensures that the GUI, overlay, and agent share a strict type-safe boundary. The protocol supports:
- State observation (
observe,observe_action_ring) - Device configuration (DPI, lighting, pairing)
- Ring interaction (hover, activate, cancel)
Because tarpc generates client and server stubs from the same definition, compile-time checks prevent drift between the agent's implementation and the frontends' expectations.
Summary
- Transport: Cross-platform local sockets (Unix domain/named pipes) with length-delimited bincode framing, implemented in
crates/openlogi-ipc/src/transport.rs. - Handshake: Clients verify
protocol_version()(currently 29) then declare their role viaClientKindenum. - GUI Pattern: Long-polling via
observe()in a dedicated Tokio thread, with commands and results flowing throughAgentClientandmpscchannels. - Overlay Pattern: Concurrent
poll_invocations(blocking onobserve_action_ring) andsend_commandstasks for real-time input handling. - Safety: Protocol version mismatches trigger immediate client termination or agent restart, preventing undefined behavior across API boundaries.
Frequently Asked Questions
What transport protocol does OpenLogi use for IPC?
OpenLogi uses tarpc (a Tokio-based RPC library) over a raw byte stream provided by local sockets. The transport layer adds length-delimited framing and bincode serialization on top of Unix-domain sockets (macOS/Linux) or named pipes (Windows), as defined in crates/openlogi-ipc/src/transport.rs.
How does the agent distinguish between GUI and overlay clients?
During the initial handshake, clients call declare_client() with a ClientKind variant—either Gui or Overlay—defined in crates/openlogi-ipc/src/ipc.rs. The agent uses this declaration to selectively enable features; for example, it may keep power-hungry subsystems dormant when only the overlay is connected.
What happens if the protocol version mismatches between client and agent?
The client checks PROTOCOL_VERSION (currently 29) immediately after connecting. If the agent reports a different version, the GUI triggers an agent restart to align binaries, while the overlay process exits cleanly. This prevents serialization errors from corrupting device state.
Why does the GUI use a blocking observe call instead of polling?
The observe() method implements long-polling: it blocks until the agent's observable state generation counter increments or a timeout expires. This reduces CPU usage and latency compared to traditional polling loops, ensuring the GUI reflects hardware changes within milliseconds without consuming resources during idle periods.
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 →