Skip to content

Commit 16c7ca0

Browse files
committed
feat: implement Varlink API to register and manage hardware controller hints across devices
1 parent bfb48e4 commit 16c7ca0

17 files changed

Lines changed: 564 additions & 6 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ AI Agents can leverage `contextd` to:
4646
- **Hardware Debugging**: If a user asks "Why isn't my mouse working?", an agent can check `ListDevices()` to see if the hardware is detected and if permissions (`uaccess`) are correct.
4747
- **System Sanity Checks**: Use `GetDiagnostics()` to verify if a user's system meets specific game requirements (VRAM, RAM) or to identify if they are missing critical graphics drivers/libraries (Vulkan/OpenGL).
4848
- **Game Stats Integration**: Use the detected AppID to fetch external game metadata or launch specific companion overlays.
49+
- **Cooperative Hardware Management**: Use `RegisterController()` to signal that your agent is managing specific hardware (e.g., "AI Macro Engine"). Other apps like Solaar or OpenRGB will see this hint and can avoid conflicting configurations or suggest troubleshooting steps to the user.

docs/controller_hints.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# Hardware Controller Hints
2+
3+
`contextd` provides a public endpoint for device controller software (e.g., RGB controllers, DPI managers, macro engines) to register their "interest" in specific hardware devices.
4+
5+
This is a **cooperative, non-authoritative** system. It does not lock devices or prevent other apps from accessing them. Instead, it serves as a discovery mechanism so that different applications can be aware of each other and help the user resolve potential conflicts.
6+
7+
## How it Works
8+
9+
1. **Registration**: An application calls `RegisterController()` providing its PID, capabilities, and a list of device paths it is interested in.
10+
2. **Discovery**: Other applications calling `ListDevices()` or `ListRGBDevices()` will see an embedded list of "interested" controllers for each device.
11+
3. **Lifecycle**: `contextd` monitors the PIDs of registered controllers. If a process terminates, its hint is automatically removed from the system.
12+
13+
## Varlink Interface
14+
15+
### `Controller` Type
16+
17+
| Field | Type | Description |
18+
| :--- | :--- | :--- |
19+
| `name` | `string` | Human-readable name (e.g., "OpenRGB") |
20+
| `version` | `?string` | Optional version string |
21+
| `pid` | `int` | Process ID of the controller |
22+
| `description` | `?string` | Short description of what the app is doing |
23+
| `website` | `?string` | Link to documentation or help |
24+
| `capabilities` | `[]string` | List of features (e.g., `["rgb", "dpi", "battery"]`) |
25+
| `interested_devices`| `[]string` | List of device paths (e.g., `["/dev/hidraw0"]`) |
26+
27+
### Methods
28+
29+
* `RegisterController(controller: Controller)`: Register or update a hint.
30+
* `UnregisterController(pid: int)`: Manually remove a hint.
31+
* `ListControllers()`: List all active hints.
32+
33+
## Example Use Cases
34+
35+
### Troubleshooting Conflicts
36+
If Solaar is running and managing a Logitech mouse, and a user opens another configuration tool that fails to apply settings, that tool can check `ListDevices()` and see:
37+
> "Hey, Solaar (PID 1234) is already interested in this device. You might need to close it or adjust its settings."
38+
39+
### Cooperative RGB
40+
Multiple RGB applications can signal which devices they are "watching". While `contextd` doesn't enforce exclusive access, it provides the metadata needed for apps to "play nice" or for the user to understand why their lighting is flickering (due to multiple managers).
41+
42+
## Testing with `contextctl`
43+
44+
You can register a temporary hint for your current shell using the provided helper script:
45+
46+
```bash
47+
./scripts/contextctl.sh hint "MyAgent" "/dev/hidraw0"
48+
```
49+
50+
This will register the hint and keep it active until you press `Ctrl+C`.

examples/README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,18 @@ Because `contextd` uses [Varlink](https://varlink.org) over standard Unix socket
1010
Python is a great choice for scripting against `contextd` because it has built-in support for Unix sockets and JSON, requiring zero external dependencies.
1111
- **`python/get_active_game.py`**: Queries the core daemon for the currently foregrounded game.
1212
- **`python/subscribe_rgb.py`**: Subscribes to real-time ambient lighting updates from the RGB observer socket.
13+
- **`python/register_controller.py`**: Demonstrates how to register a hardware controller "hint" to coordinate with other apps.
1314

1415
### 2. Shell
1516
For simple queries in bash scripts, the `varlinkctl` tool (usually installed alongside varlink) allows one-line interactions.
1617
- **`shell/get_diagnostics.sh`**: Uses `varlinkctl` to query system diagnostics.
18+
- **`shell/register_controller.sh`**: Registers a temporary hint using a bash script.
1719

1820
### 3. Rust
1921
While the `contextd` daemon itself is written in Rust and uses the `varlink` crate, you can also interact with it using nothing but the standard library and `serde_json`.
2022
- **`rust/src/main.rs`**: A standard Cargo project showing how to connect to the Unix socket and parse a JSON response.
21-
*Run it with:* `cd rust && cargo run`
23+
- **`rust/src/bin/register_controller.rs`**: Shows how to register a controller hint using raw Unix sockets and Serde.
24+
*Run it with:* `cd rust && cargo run --bin register_controller`
2225

2326
### 4. C
2427
A minimal C example demonstrating how to use POSIX sockets to send a Varlink JSON request and read the response.
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Example Python script to register a controller hint with contextd using varlink.
4+
Requires: pip install varlink
5+
"""
6+
7+
import os
8+
import sys
9+
import varlink
10+
11+
def main():
12+
# Contextd public socket address
13+
address = "unix:/run/contextd/public/contextd.socket"
14+
15+
try:
16+
with varlink.Client(address) as client:
17+
contextd = client.open('com.performativenonsense.contextd')
18+
19+
# Prepare the controller hint
20+
hint = {
21+
"name": "Python Controller Example",
22+
"version": "1.0.0",
23+
"pid": os.getpid(),
24+
"description": "An example script showing cooperative management",
25+
"website": "https://github.com/shanefagan/contextd",
26+
"capabilities": ["test", "demo"],
27+
"interested_devices": ["/dev/hidraw0"] # Replace with a real device path
28+
}
29+
30+
print(f"Registering hint for PID {os.getpid()}...")
31+
contextd.RegisterController(hint)
32+
33+
print("\nActive Controllers:")
34+
controllers = contextd.ListControllers()
35+
for c in controllers:
36+
print(f"- {c['name']} (PID: {c['pid']}) - {c.get('description', '')}")
37+
38+
print("\nDevices with hints:")
39+
devices = contextd.ListDevices()
40+
for d in devices:
41+
if d.get('controllers'):
42+
print(f"- {d['name']} ({d['path']})")
43+
for c in d['controllers']:
44+
print(f" * Managed by: {c['name']}")
45+
46+
input("\nPress Enter to unregister and exit...")
47+
contextd.UnregisterController(os.getpid())
48+
49+
except varlink.VarlinkError as e:
50+
print(f"Varlink error: {e}")
51+
except ConnectionRefusedError:
52+
print(f"Could not connect to contextd at {address}. Is it running?")
53+
54+
if __name__ == "__main__":
55+
main()

examples/rust/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,7 @@ version = "0.1.0"
44
edition = "2021"
55

66
[dependencies]
7+
serde = { version = "1.0", features = ["derive"] }
78
serde_json = "1.0"
9+
varlink = "13.0"
10+
anyhow = "1.0"
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use anyhow::Result;
2+
use serde::{Deserialize, Serialize};
3+
use std::io::{Read, Write};
4+
use std::os::unix::net::UnixStream;
5+
use std::process;
6+
7+
/// Minimal representation of the Controller type for the example
8+
#[derive(Serialize, Deserialize, Debug, Clone)]
9+
struct Controller {
10+
name: String,
11+
version: Option<String>,
12+
pid: i64,
13+
description: Option<String>,
14+
website: Option<String>,
15+
capabilities: Vec<String>,
16+
interested_devices: Vec<String>,
17+
}
18+
19+
#[derive(Serialize)]
20+
struct RegisterArgs {
21+
controller: Controller,
22+
}
23+
24+
#[derive(Serialize)]
25+
struct UnregisterArgs {
26+
pid: i64,
27+
}
28+
29+
#[derive(Serialize)]
30+
struct VarlinkRequest<T> {
31+
method: String,
32+
parameters: T,
33+
}
34+
35+
fn main() -> Result<()> {
36+
let socket_path = "/run/contextd/public/contextd.socket";
37+
let mut stream = UnixStream::connect(socket_path)?;
38+
39+
let pid = process::id() as i64;
40+
let hint = Controller {
41+
name: "Rust Example Agent".to_string(),
42+
version: Some("0.1.0".to_string()),
43+
pid,
44+
description: Some("Cooperative hardware manager example in Rust".to_string()),
45+
website: None,
46+
capabilities: vec!["rust".to_string(), "demo".to_string()],
47+
interested_devices: vec!["/dev/hidraw0".to_string()],
48+
};
49+
50+
println!("Registering hint for PID {}...", pid);
51+
52+
// Register
53+
let req = VarlinkRequest {
54+
method: "com.performativenonsense.contextd.RegisterController".to_string(),
55+
parameters: RegisterArgs { controller: hint },
56+
};
57+
58+
let mut payload = serde_json::to_vec(&req)?;
59+
payload.push(0); // Varlink uses null-terminator for messages
60+
stream.write_all(&payload)?;
61+
62+
// Read the response (Varlink requires reading the response to ensure the call completed)
63+
let mut buf = [0u8; 1024];
64+
let n = stream.read(&mut buf)?;
65+
println!("Response: {}", String::from_utf8_lossy(&buf[..n]).trim_end_matches('\0'));
66+
67+
println!("\nRegistered successfully. You can verify this by running:");
68+
println!(" varlinkctl call unix:{} com.performativenonsense.contextd.ListControllers", socket_path);
69+
70+
println!("\nPress Enter to unregister and exit...");
71+
let mut input = String::new();
72+
std::io::stdin().read_line(&mut input)?;
73+
74+
// Unregister cleanly
75+
let mut stream = UnixStream::connect(socket_path)?;
76+
let req = VarlinkRequest {
77+
method: "com.performativenonsense.contextd.UnregisterController".to_string(),
78+
parameters: UnregisterArgs { pid },
79+
};
80+
let mut payload = serde_json::to_vec(&req)?;
81+
payload.push(0);
82+
stream.write_all(&payload)?;
83+
84+
println!("Unregistered. Goodbye!");
85+
86+
Ok(())
87+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#!/bin/bash
2+
# Example script to register a controller hint with contextd
3+
4+
# Get current PID
5+
PID=$$
6+
7+
# Register Solaar-like hint
8+
varlink call unix:/run/contextd/public/contextd.socket/com.performativenonsense.contextd.RegisterController '{
9+
"controller": {
10+
"name": "Solaar",
11+
"version": "1.1.13",
12+
"pid": '$PID',
13+
"description": "Logitech device manager",
14+
"website": "https://pwr-solaar.github.io/Solaar/",
15+
"capabilities": ["battery", "input", "rgb"],
16+
"interested_devices": ["/dev/hidraw0", "/dev/hidraw1"]
17+
}
18+
}'
19+
20+
echo "Registered Solaar hint for PID $PID"
21+
echo "Press Enter to list ALL CONTROLLERS..."
22+
read
23+
varlink call unix:/run/contextd/public/contextd.socket/com.performativenonsense.contextd.ListControllers
24+
25+
echo "Press Enter to list ALL DEVICES (will show Solaar hint next to /dev/hidraw0)..."
26+
read
27+
varlink call unix:/run/contextd/public/contextd.socket/com.performativenonsense.contextd.ListDevices
28+
29+
echo "Press Enter to unregister and exit..."
30+
read
31+
32+
varlink call unix:/run/contextd/public/contextd.socket/com.performativenonsense.contextd.UnregisterController '{"pid": '$PID'}'

scripts/contextctl.sh

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ RGB_OBS_ADDR="unix:/run/contextd/public/contextd-rgb-observer.socket"
66
RGB_CTRL_ADDR="unix:/run/contextd/private/contextd-rgb-control.socket"
77

88
usage() {
9-
echo "Usage: $0 [active|list-games|list-devices|list-rgb|diagnostics|rgb-get|rgb-set|rgb-set-matrix|rgb-subscribe]"
9+
echo "Usage: $0 [active|list-games|list-devices|list-rgb|list-controllers|hint|diagnostics|rgb-get|rgb-set|rgb-set-matrix|rgb-subscribe]"
10+
echo " list-controllers List apps that have registered interest in devices"
11+
echo " hint NAME DEVICE... Register a quick hint for the current shell"
1012
echo " rgb-get/subscribe use the PUBLIC observer socket (0666)"
1113
echo " rgb-set uses the PRIVATE control socket (0660, contextd-rgb group)"
1214
echo " rgb-set R G B [A] (Alpha defaults to 255)"
@@ -34,6 +36,30 @@ case $CMD in
3436
list-rgb)
3537
varlinkctl call $ADDR com.performativenonsense.contextd.ListRGBDevices "{}"
3638
;;
39+
list-controllers)
40+
varlinkctl call $ADDR com.performativenonsense.contextd.ListControllers "{}"
41+
;;
42+
hint)
43+
NAME=$1; shift
44+
DEVICES=""
45+
for dev in "$@"; do
46+
if [ -n "$DEVICES" ]; then DEVICES="$DEVICES, "; fi
47+
DEVICES="$DEVICES\"$dev\""
48+
done
49+
varlinkctl call $ADDR com.performativenonsense.contextd.RegisterController "{
50+
\"controller\": {
51+
\"name\": \"$NAME\",
52+
\"pid\": $$,
53+
\"description\": \"Manual hint from contextctl\",
54+
\"capabilities\": [\"manual\"],
55+
\"interested_devices\": [$DEVICES]
56+
}
57+
}"
58+
echo "Hint registered for PID $$. Press Ctrl+C to unregister and exit."
59+
# Keep alive until interrupted
60+
trap "varlinkctl call $ADDR com.performativenonsense.contextd.UnregisterController '{\"pid\": $$}'; exit" SIGINT SIGTERM
61+
while true; do sleep 1; done
62+
;;
3763
diagnostics)
3864
varlinkctl call $ADDR com.performativenonsense.contextd.GetDiagnostics "{}"
3965
;;

0 commit comments

Comments
 (0)