elfdeckai 1.0.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- elfdeckai-1.0.4/LICENSE +21 -0
- elfdeckai-1.0.4/PKG-INFO +230 -0
- elfdeckai-1.0.4/README.md +212 -0
- elfdeckai-1.0.4/elfdeckai.egg-info/PKG-INFO +230 -0
- elfdeckai-1.0.4/elfdeckai.egg-info/SOURCES.txt +16 -0
- elfdeckai-1.0.4/elfdeckai.egg-info/dependency_links.txt +1 -0
- elfdeckai-1.0.4/elfdeckai.egg-info/entry_points.txt +5 -0
- elfdeckai-1.0.4/elfdeckai.egg-info/top_level.txt +4 -0
- elfdeckai-1.0.4/elfdeckai_mcp.py +331 -0
- elfdeckai-1.0.4/elfkey.py +260 -0
- elfdeckai-1.0.4/elfmouse.py +302 -0
- elfdeckai-1.0.4/elfpad.py +379 -0
- elfdeckai-1.0.4/pyproject.toml +34 -0
- elfdeckai-1.0.4/setup.cfg +4 -0
- elfdeckai-1.0.4/tests/test_elfkey.py +261 -0
- elfdeckai-1.0.4/tests/test_elfmouse.py +140 -0
- elfdeckai-1.0.4/tests/test_elfpad.py +213 -0
- elfdeckai-1.0.4/tests/test_mcp.py +274 -0
elfdeckai-1.0.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Garth Elfstrom
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
elfdeckai-1.0.4/PKG-INFO
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: elfdeckai
|
|
3
|
+
Version: 1.0.4
|
|
4
|
+
Summary: Virtual keyboard, gamepad and mouse for AI agents on Linux / Steam Deck โ zero-dependency uinput daemons with an MCP server.
|
|
5
|
+
Author: Garth Elfstrom
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Garthnesss/ElfDeckAI
|
|
8
|
+
Project-URL: Repository, https://github.com/Garthnesss/ElfDeckAI
|
|
9
|
+
Project-URL: Issues, https://github.com/Garthnesss/ElfDeckAI/issues
|
|
10
|
+
Keywords: mcp,model-context-protocol,llm,ai-agent,steam-deck,uinput,gamepad,automation
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# ๐ฎ ElfdeckAI
|
|
20
|
+
|
|
21
|
+
### Your AI can *see* the game. Now it can hold the controller.
|
|
22
|
+
|
|
23
|
+
[](LICENSE)
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
**ElfdeckAI gives an LLM agent hands on real hardware.** Three tiny
|
|
29
|
+
daemons turn one-line commands โ from a shell, a script, or native
|
|
30
|
+
**MCP tool calls** โ into real keyboard, gamepad and mouse input on a
|
|
31
|
+
Linux machine:
|
|
32
|
+
|
|
33
|
+
- โจ๏ธ **ElfKey** โ a virtual keyboard on `/dev/uinput`
|
|
34
|
+
- ๐น๏ธ **ElfPad** โ a virtual gamepad (Xbox-class: buttons, analog triggers,
|
|
35
|
+
two sticks, D-pad)
|
|
36
|
+
- ๐ฑ๏ธ **ElfMouse** โ a virtual mouse (relative pointer, buttons, wheel)
|
|
37
|
+
|
|
38
|
+
Every "LLM plays Pokรฉmon" project you've seen plugs into an **emulator's
|
|
39
|
+
API**. ElfdeckAI presses buttons on a **real device** โ proven live when an
|
|
40
|
+
LLM played Pokรฉmon FireRed on a physical Steam Deck end-to-end through
|
|
41
|
+
ElfKey: full intro, starter picked, first rival battle won, game saved.
|
|
42
|
+
No emulator *API*, no RAM reading, no capture card โ the game runs on
|
|
43
|
+
the real device (inside RetroArch, in that run); the agent's only
|
|
44
|
+
channel in is real input, and its only view is a stream for eyes.
|
|
45
|
+
([How it drives](notes/driving.md))
|
|
46
|
+
|
|
47
|
+
## โก Run it yourself (60 seconds)
|
|
48
|
+
|
|
49
|
+
```sh
|
|
50
|
+
pip install git+https://github.com/Garthnesss/ElfDeckAI
|
|
51
|
+
|
|
52
|
+
elfkey & # virtual keyboard (command: elfkey)
|
|
53
|
+
elfpad & # virtual gamepad (command: elfpad)
|
|
54
|
+
elfmouse & # virtual mouse (command: elfmouse)
|
|
55
|
+
|
|
56
|
+
echo "key x 150" > /tmp/elfkey.fifo # tap X
|
|
57
|
+
echo "press a" > /tmp/elfpad.fifo # tap A
|
|
58
|
+
echo "move 300 -100" > /tmp/elfmouse.fifo # flick the pointer
|
|
59
|
+
echo "click left" > /tmp/elfmouse.fifo # click
|
|
60
|
+
|
|
61
|
+
grep -E "ElfKey|ElfPad|ElfMouse" /proc/bus/input/devices # they're really there
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
That's the whole install. Three single-file Python programs, **standard
|
|
65
|
+
library only** โ nothing to build, no dependencies to resolve. State
|
|
66
|
+
lives in `/tmp`; reboot and it's gone. Prefer no pip at all? Clone the
|
|
67
|
+
repo and run `python3 elfkey.py` โ same thing.
|
|
68
|
+
|
|
69
|
+
For your agent, the pip install also provides the MCP server as a
|
|
70
|
+
command: `"command": "elfdeckai-mcp"` (details below).
|
|
71
|
+
|
|
72
|
+
> **Steam Deck?** You're already done โ `/dev/uinput` is user-writable
|
|
73
|
+
> there out of the box. On other Linux machines, install the included
|
|
74
|
+
> udev rule once (see [Install](#install)).
|
|
75
|
+
|
|
76
|
+
## ๐ค Give it to your agent (MCP)
|
|
77
|
+
|
|
78
|
+
`elfdeckai_mcp.py` is a zero-dependency [Model Context
|
|
79
|
+
Protocol](https://modelcontextprotocol.io) server that exposes all
|
|
80
|
+
three devices as 18 native tools โ `key_tap`, `pad_press`, `pad_stick`,
|
|
81
|
+
`mouse_move`, `mouse_click`, `pad_reset`, `key_release`, โฆ โ for Claude,
|
|
82
|
+
Hermes, or any MCP client:
|
|
83
|
+
|
|
84
|
+
```json
|
|
85
|
+
{"mcpServers": {"elfdeckai": {"command": "elfdeckai-mcp"}}}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
(No pip? `"command": "python3", "args": ["/path/to/elfdeckai_mcp.py"]`
|
|
89
|
+
works identically.)
|
|
90
|
+
|
|
91
|
+
Start the daemons, add that to your client's config, and your agent can
|
|
92
|
+
press buttons the same way it calls any other tool. Every call is
|
|
93
|
+
validated by the daemons' own input contract before it touches a FIFO,
|
|
94
|
+
and a stopped daemon fails the call cleanly instead of hanging your agent.
|
|
95
|
+
|
|
96
|
+
## ๐๏ธ Commands
|
|
97
|
+
|
|
98
|
+
### ElfKey โ `/tmp/elfkey.fifo`
|
|
99
|
+
|
|
100
|
+
| Command | Effect |
|
|
101
|
+
|---|---|
|
|
102
|
+
| `key <name> [ms]` | tap a key (default 120 ms) |
|
|
103
|
+
| `down <name>` / `up <name>` | hold / release |
|
|
104
|
+
| `release` | let go of every held key at once (the unstick command) |
|
|
105
|
+
| `quit` | release everything and exit |
|
|
106
|
+
|
|
107
|
+
Keys: `a`โ`z`, `0`โ`9`, arrows, `enter`, `esc`, `space`, `tab`, `shift`,
|
|
108
|
+
`rshift`, `ctrl`, `alt`, punctuation, `f1`โ`f10`, `home`, `end`, `insert`,
|
|
109
|
+
`delete`, โฆ (full table in the source).
|
|
110
|
+
|
|
111
|
+
### ElfPad โ `/tmp/elfpad.fifo`
|
|
112
|
+
|
|
113
|
+
| Command | Effect |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `press <btn> [ms]` | tap a button โ or a D-pad direction |
|
|
116
|
+
| `down <btn>` / `up <btn>` | hold / release |
|
|
117
|
+
| `stick left\|right <x> <y> [ms]` | analog stick, โ100..100 % per axis; `ms` = auto-recenter after |
|
|
118
|
+
| `trigger left\|right <0..100> [ms]` | analog trigger; `ms` = auto-release after |
|
|
119
|
+
| `center` | sticks, triggers, D-pad to neutral |
|
|
120
|
+
| `reset` | release everything |
|
|
121
|
+
| `quit` | release everything and exit |
|
|
122
|
+
|
|
123
|
+
Buttons: `a b x y lb rb l2 r2 select start guide l3 r3`. ElfPad presents
|
|
124
|
+
as an Xbox-360-class pad, so SDL, Steam Input and RetroArch map it the
|
|
125
|
+
standard way with zero configuration. `l2`/`r2` are analog-only, exactly
|
|
126
|
+
like the real pad โ there are deliberately no digital trigger buttons,
|
|
127
|
+
because declaring them shifts Select/Start/Guide two button indices off
|
|
128
|
+
the stock layout (verified against a real 360 pad on a Deck).
|
|
129
|
+
|
|
130
|
+
### ElfMouse โ `/tmp/elfmouse.fifo`
|
|
131
|
+
|
|
132
|
+
| Command | Effect |
|
|
133
|
+
|---|---|
|
|
134
|
+
| `move <dx> <dy> [glide_ms]` | move the pointer (relative); `glide_ms` = glide there over time |
|
|
135
|
+
| `click <btn> [ms]` | click `left` / `right` / `middle` |
|
|
136
|
+
| `down <btn>` / `up <btn>` | hold / release a button โ drags are `down`, `move`, `up` |
|
|
137
|
+
| `scroll <ticks>` / `hscroll <ticks>` | wheel / horizontal wheel (positive = up / right) |
|
|
138
|
+
| `release` | let go of every held button at once (the unstick command) |
|
|
139
|
+
| `quit` | release any held button and exit |
|
|
140
|
+
|
|
141
|
+
Movement is relative, like a real mouse โ pair it with screenshots and
|
|
142
|
+
the same look โ move โ verify loop the rest of the set uses. Sign
|
|
143
|
+
convention: negative `dy` is up, positive is down. Units are relative
|
|
144
|
+
counts, not guaranteed screen pixels โ pointer acceleration stretches
|
|
145
|
+
fast moves โ so small moves plus a fresh screenshot beat one big leap. Per-command
|
|
146
|
+
bounds (ยฑ10 000 px, ยฑ120 ticks) keep a confused call from flinging the
|
|
147
|
+
pointer into orbit; chain commands for long travel.
|
|
148
|
+
|
|
149
|
+
## ๐ก๏ธ Built for callers that hallucinate
|
|
150
|
+
|
|
151
|
+
Letting a language model press buttons is safe only if the keyboard
|
|
152
|
+
itself can't be talked into a bad state. So:
|
|
153
|
+
|
|
154
|
+
- **Bad input never kills the daemon.** Every line passes a strict
|
|
155
|
+
parser; malformed commands are logged and refused. (`key x abc` and
|
|
156
|
+
`key x 999999999` were real crash/wedge bugs in the first version โ
|
|
157
|
+
the tests now pin them shut.)
|
|
158
|
+
- **Holds are bounded.** A tap is at most 5 s. Longer holds use
|
|
159
|
+
`down`/`up`, which the caller controls.
|
|
160
|
+
- **Nothing gets stuck.** Timed moves restore in a `finally` block; held
|
|
161
|
+
inputs are tracked; on `quit`, SIGTERM or SIGINT, everything is
|
|
162
|
+
released and recentered before the device is destroyed.
|
|
163
|
+
- **The FIFO is owner-only (0600).** Only your user can inject input.
|
|
164
|
+
- **The server survives garbage.** Malformed JSON and non-object
|
|
165
|
+
requests get proper JSON-RPC protocol errors; one bad line โ or a
|
|
166
|
+
caller dying mid-hold โ can't wedge the session. `key_release`,
|
|
167
|
+
`mouse_release` and `pad_reset` are the unstick buttons โ a release
|
|
168
|
+
queues behind at most the one hold currently executing (bounded,
|
|
169
|
+
โค 5 s), and every accepted command is logged to stderr, so what the
|
|
170
|
+
agent pressed is auditable afterwards.
|
|
171
|
+
- **Scope honesty:** these are real OS-level input devices. They type
|
|
172
|
+
and click in whatever window has focus โ a game, but equally a
|
|
173
|
+
terminal or a browser. ElfdeckAI makes that input *un-mess-up-able*
|
|
174
|
+
(bounded, validated, releasable); deciding *where* an agent may act
|
|
175
|
+
is the operator's call. Run agents you trust, on sessions you'd
|
|
176
|
+
watch.
|
|
177
|
+
|
|
178
|
+
## ๐ค Why notโฆ?
|
|
179
|
+
|
|
180
|
+
| Approach | Why it falls short |
|
|
181
|
+
|---|---|
|
|
182
|
+
| Emulator APIs (PyBoy, mGBA scripting) | Only exists inside an emulator. Your Deck is not an emulator. |
|
|
183
|
+
| Sending keys through Moonlight/streaming | Clicks get forwarded; synthetic keyboard events don't (verified). |
|
|
184
|
+
| xdotool / desktop automation | Can't reach native Wayland clients. |
|
|
185
|
+
| ydotool / dotool | The real incumbents โ same uinput trick, keyboard + mouse. But: raw keycodes, a root daemon, no gamepad, and nothing agent-facing (no MCP, no safety bounds, no unstick). |
|
|
186
|
+
| KVM gadget / controller-spoofing hardware | Works, but it's extra hardware on your desk doing what 200 lines of Python do on the device itself. |
|
|
187
|
+
|
|
188
|
+
## Install
|
|
189
|
+
|
|
190
|
+
**Requirements:** Linux (64-bit) with `/dev/uinput`, Python 3.8+. Nothing else.
|
|
191
|
+
|
|
192
|
+
Anywhere `/dev/uinput` isn't user-writable, install the udev rule once:
|
|
193
|
+
|
|
194
|
+
```sh
|
|
195
|
+
sudo cp 70-elfdeckai-uinput.rules /etc/udev/rules.d/
|
|
196
|
+
sudo udevadm control --reload-rules && sudo udevadm trigger
|
|
197
|
+
getfacl /dev/uinput # verify: your user should appear in the ACL
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Run persistently as user services (expects the repo at `~/elfdeckai/` โ clone with `git clone https://github.com/Garthnesss/ElfDeckAI ~/elfdeckai`):
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
cp systemd/*.service ~/.config/systemd/user/
|
|
204
|
+
systemctl --user enable --now elfkey.service elfpad.service elfmouse.service
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Tests
|
|
208
|
+
|
|
209
|
+
```sh
|
|
210
|
+
python3 -m pytest # 146 tests: all three input contracts, device behaviour
|
|
211
|
+
# against a fake uinput, and the MCP server spoken
|
|
212
|
+
# to over real JSON-RPC
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Roadmap
|
|
216
|
+
|
|
217
|
+
- One combined daemon (keyboard + pad + mouse in a single process).
|
|
218
|
+
- PyPI release, so `pip install elfdeckai` works without the git URL.
|
|
219
|
+
- Demo clip: the Pokรฉmon run, on camera.
|
|
220
|
+
|
|
221
|
+
## License
|
|
222
|
+
|
|
223
|
+
MIT โ see [LICENSE](LICENSE). Steal it, ship it, teach your agent to
|
|
224
|
+
speedrun something.
|
|
225
|
+
|
|
226
|
+
## Author
|
|
227
|
+
|
|
228
|
+
Created and maintained by **Garth Elfstrom**
|
|
229
|
+
([@Garthnesss](https://github.com/Garthnesss)). Issues and PRs welcome
|
|
230
|
+
on the repo.
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# ๐ฎ ElfdeckAI
|
|
2
|
+
|
|
3
|
+
### Your AI can *see* the game. Now it can hold the controller.
|
|
4
|
+
|
|
5
|
+
[](LICENSE)
|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
**ElfdeckAI gives an LLM agent hands on real hardware.** Three tiny
|
|
11
|
+
daemons turn one-line commands โ from a shell, a script, or native
|
|
12
|
+
**MCP tool calls** โ into real keyboard, gamepad and mouse input on a
|
|
13
|
+
Linux machine:
|
|
14
|
+
|
|
15
|
+
- โจ๏ธ **ElfKey** โ a virtual keyboard on `/dev/uinput`
|
|
16
|
+
- ๐น๏ธ **ElfPad** โ a virtual gamepad (Xbox-class: buttons, analog triggers,
|
|
17
|
+
two sticks, D-pad)
|
|
18
|
+
- ๐ฑ๏ธ **ElfMouse** โ a virtual mouse (relative pointer, buttons, wheel)
|
|
19
|
+
|
|
20
|
+
Every "LLM plays Pokรฉmon" project you've seen plugs into an **emulator's
|
|
21
|
+
API**. ElfdeckAI presses buttons on a **real device** โ proven live when an
|
|
22
|
+
LLM played Pokรฉmon FireRed on a physical Steam Deck end-to-end through
|
|
23
|
+
ElfKey: full intro, starter picked, first rival battle won, game saved.
|
|
24
|
+
No emulator *API*, no RAM reading, no capture card โ the game runs on
|
|
25
|
+
the real device (inside RetroArch, in that run); the agent's only
|
|
26
|
+
channel in is real input, and its only view is a stream for eyes.
|
|
27
|
+
([How it drives](notes/driving.md))
|
|
28
|
+
|
|
29
|
+
## โก Run it yourself (60 seconds)
|
|
30
|
+
|
|
31
|
+
```sh
|
|
32
|
+
pip install git+https://github.com/Garthnesss/ElfDeckAI
|
|
33
|
+
|
|
34
|
+
elfkey & # virtual keyboard (command: elfkey)
|
|
35
|
+
elfpad & # virtual gamepad (command: elfpad)
|
|
36
|
+
elfmouse & # virtual mouse (command: elfmouse)
|
|
37
|
+
|
|
38
|
+
echo "key x 150" > /tmp/elfkey.fifo # tap X
|
|
39
|
+
echo "press a" > /tmp/elfpad.fifo # tap A
|
|
40
|
+
echo "move 300 -100" > /tmp/elfmouse.fifo # flick the pointer
|
|
41
|
+
echo "click left" > /tmp/elfmouse.fifo # click
|
|
42
|
+
|
|
43
|
+
grep -E "ElfKey|ElfPad|ElfMouse" /proc/bus/input/devices # they're really there
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
That's the whole install. Three single-file Python programs, **standard
|
|
47
|
+
library only** โ nothing to build, no dependencies to resolve. State
|
|
48
|
+
lives in `/tmp`; reboot and it's gone. Prefer no pip at all? Clone the
|
|
49
|
+
repo and run `python3 elfkey.py` โ same thing.
|
|
50
|
+
|
|
51
|
+
For your agent, the pip install also provides the MCP server as a
|
|
52
|
+
command: `"command": "elfdeckai-mcp"` (details below).
|
|
53
|
+
|
|
54
|
+
> **Steam Deck?** You're already done โ `/dev/uinput` is user-writable
|
|
55
|
+
> there out of the box. On other Linux machines, install the included
|
|
56
|
+
> udev rule once (see [Install](#install)).
|
|
57
|
+
|
|
58
|
+
## ๐ค Give it to your agent (MCP)
|
|
59
|
+
|
|
60
|
+
`elfdeckai_mcp.py` is a zero-dependency [Model Context
|
|
61
|
+
Protocol](https://modelcontextprotocol.io) server that exposes all
|
|
62
|
+
three devices as 18 native tools โ `key_tap`, `pad_press`, `pad_stick`,
|
|
63
|
+
`mouse_move`, `mouse_click`, `pad_reset`, `key_release`, โฆ โ for Claude,
|
|
64
|
+
Hermes, or any MCP client:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{"mcpServers": {"elfdeckai": {"command": "elfdeckai-mcp"}}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(No pip? `"command": "python3", "args": ["/path/to/elfdeckai_mcp.py"]`
|
|
71
|
+
works identically.)
|
|
72
|
+
|
|
73
|
+
Start the daemons, add that to your client's config, and your agent can
|
|
74
|
+
press buttons the same way it calls any other tool. Every call is
|
|
75
|
+
validated by the daemons' own input contract before it touches a FIFO,
|
|
76
|
+
and a stopped daemon fails the call cleanly instead of hanging your agent.
|
|
77
|
+
|
|
78
|
+
## ๐๏ธ Commands
|
|
79
|
+
|
|
80
|
+
### ElfKey โ `/tmp/elfkey.fifo`
|
|
81
|
+
|
|
82
|
+
| Command | Effect |
|
|
83
|
+
|---|---|
|
|
84
|
+
| `key <name> [ms]` | tap a key (default 120 ms) |
|
|
85
|
+
| `down <name>` / `up <name>` | hold / release |
|
|
86
|
+
| `release` | let go of every held key at once (the unstick command) |
|
|
87
|
+
| `quit` | release everything and exit |
|
|
88
|
+
|
|
89
|
+
Keys: `a`โ`z`, `0`โ`9`, arrows, `enter`, `esc`, `space`, `tab`, `shift`,
|
|
90
|
+
`rshift`, `ctrl`, `alt`, punctuation, `f1`โ`f10`, `home`, `end`, `insert`,
|
|
91
|
+
`delete`, โฆ (full table in the source).
|
|
92
|
+
|
|
93
|
+
### ElfPad โ `/tmp/elfpad.fifo`
|
|
94
|
+
|
|
95
|
+
| Command | Effect |
|
|
96
|
+
|---|---|
|
|
97
|
+
| `press <btn> [ms]` | tap a button โ or a D-pad direction |
|
|
98
|
+
| `down <btn>` / `up <btn>` | hold / release |
|
|
99
|
+
| `stick left\|right <x> <y> [ms]` | analog stick, โ100..100 % per axis; `ms` = auto-recenter after |
|
|
100
|
+
| `trigger left\|right <0..100> [ms]` | analog trigger; `ms` = auto-release after |
|
|
101
|
+
| `center` | sticks, triggers, D-pad to neutral |
|
|
102
|
+
| `reset` | release everything |
|
|
103
|
+
| `quit` | release everything and exit |
|
|
104
|
+
|
|
105
|
+
Buttons: `a b x y lb rb l2 r2 select start guide l3 r3`. ElfPad presents
|
|
106
|
+
as an Xbox-360-class pad, so SDL, Steam Input and RetroArch map it the
|
|
107
|
+
standard way with zero configuration. `l2`/`r2` are analog-only, exactly
|
|
108
|
+
like the real pad โ there are deliberately no digital trigger buttons,
|
|
109
|
+
because declaring them shifts Select/Start/Guide two button indices off
|
|
110
|
+
the stock layout (verified against a real 360 pad on a Deck).
|
|
111
|
+
|
|
112
|
+
### ElfMouse โ `/tmp/elfmouse.fifo`
|
|
113
|
+
|
|
114
|
+
| Command | Effect |
|
|
115
|
+
|---|---|
|
|
116
|
+
| `move <dx> <dy> [glide_ms]` | move the pointer (relative); `glide_ms` = glide there over time |
|
|
117
|
+
| `click <btn> [ms]` | click `left` / `right` / `middle` |
|
|
118
|
+
| `down <btn>` / `up <btn>` | hold / release a button โ drags are `down`, `move`, `up` |
|
|
119
|
+
| `scroll <ticks>` / `hscroll <ticks>` | wheel / horizontal wheel (positive = up / right) |
|
|
120
|
+
| `release` | let go of every held button at once (the unstick command) |
|
|
121
|
+
| `quit` | release any held button and exit |
|
|
122
|
+
|
|
123
|
+
Movement is relative, like a real mouse โ pair it with screenshots and
|
|
124
|
+
the same look โ move โ verify loop the rest of the set uses. Sign
|
|
125
|
+
convention: negative `dy` is up, positive is down. Units are relative
|
|
126
|
+
counts, not guaranteed screen pixels โ pointer acceleration stretches
|
|
127
|
+
fast moves โ so small moves plus a fresh screenshot beat one big leap. Per-command
|
|
128
|
+
bounds (ยฑ10 000 px, ยฑ120 ticks) keep a confused call from flinging the
|
|
129
|
+
pointer into orbit; chain commands for long travel.
|
|
130
|
+
|
|
131
|
+
## ๐ก๏ธ Built for callers that hallucinate
|
|
132
|
+
|
|
133
|
+
Letting a language model press buttons is safe only if the keyboard
|
|
134
|
+
itself can't be talked into a bad state. So:
|
|
135
|
+
|
|
136
|
+
- **Bad input never kills the daemon.** Every line passes a strict
|
|
137
|
+
parser; malformed commands are logged and refused. (`key x abc` and
|
|
138
|
+
`key x 999999999` were real crash/wedge bugs in the first version โ
|
|
139
|
+
the tests now pin them shut.)
|
|
140
|
+
- **Holds are bounded.** A tap is at most 5 s. Longer holds use
|
|
141
|
+
`down`/`up`, which the caller controls.
|
|
142
|
+
- **Nothing gets stuck.** Timed moves restore in a `finally` block; held
|
|
143
|
+
inputs are tracked; on `quit`, SIGTERM or SIGINT, everything is
|
|
144
|
+
released and recentered before the device is destroyed.
|
|
145
|
+
- **The FIFO is owner-only (0600).** Only your user can inject input.
|
|
146
|
+
- **The server survives garbage.** Malformed JSON and non-object
|
|
147
|
+
requests get proper JSON-RPC protocol errors; one bad line โ or a
|
|
148
|
+
caller dying mid-hold โ can't wedge the session. `key_release`,
|
|
149
|
+
`mouse_release` and `pad_reset` are the unstick buttons โ a release
|
|
150
|
+
queues behind at most the one hold currently executing (bounded,
|
|
151
|
+
โค 5 s), and every accepted command is logged to stderr, so what the
|
|
152
|
+
agent pressed is auditable afterwards.
|
|
153
|
+
- **Scope honesty:** these are real OS-level input devices. They type
|
|
154
|
+
and click in whatever window has focus โ a game, but equally a
|
|
155
|
+
terminal or a browser. ElfdeckAI makes that input *un-mess-up-able*
|
|
156
|
+
(bounded, validated, releasable); deciding *where* an agent may act
|
|
157
|
+
is the operator's call. Run agents you trust, on sessions you'd
|
|
158
|
+
watch.
|
|
159
|
+
|
|
160
|
+
## ๐ค Why notโฆ?
|
|
161
|
+
|
|
162
|
+
| Approach | Why it falls short |
|
|
163
|
+
|---|---|
|
|
164
|
+
| Emulator APIs (PyBoy, mGBA scripting) | Only exists inside an emulator. Your Deck is not an emulator. |
|
|
165
|
+
| Sending keys through Moonlight/streaming | Clicks get forwarded; synthetic keyboard events don't (verified). |
|
|
166
|
+
| xdotool / desktop automation | Can't reach native Wayland clients. |
|
|
167
|
+
| ydotool / dotool | The real incumbents โ same uinput trick, keyboard + mouse. But: raw keycodes, a root daemon, no gamepad, and nothing agent-facing (no MCP, no safety bounds, no unstick). |
|
|
168
|
+
| KVM gadget / controller-spoofing hardware | Works, but it's extra hardware on your desk doing what 200 lines of Python do on the device itself. |
|
|
169
|
+
|
|
170
|
+
## Install
|
|
171
|
+
|
|
172
|
+
**Requirements:** Linux (64-bit) with `/dev/uinput`, Python 3.8+. Nothing else.
|
|
173
|
+
|
|
174
|
+
Anywhere `/dev/uinput` isn't user-writable, install the udev rule once:
|
|
175
|
+
|
|
176
|
+
```sh
|
|
177
|
+
sudo cp 70-elfdeckai-uinput.rules /etc/udev/rules.d/
|
|
178
|
+
sudo udevadm control --reload-rules && sudo udevadm trigger
|
|
179
|
+
getfacl /dev/uinput # verify: your user should appear in the ACL
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
Run persistently as user services (expects the repo at `~/elfdeckai/` โ clone with `git clone https://github.com/Garthnesss/ElfDeckAI ~/elfdeckai`):
|
|
183
|
+
|
|
184
|
+
```sh
|
|
185
|
+
cp systemd/*.service ~/.config/systemd/user/
|
|
186
|
+
systemctl --user enable --now elfkey.service elfpad.service elfmouse.service
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
## Tests
|
|
190
|
+
|
|
191
|
+
```sh
|
|
192
|
+
python3 -m pytest # 146 tests: all three input contracts, device behaviour
|
|
193
|
+
# against a fake uinput, and the MCP server spoken
|
|
194
|
+
# to over real JSON-RPC
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Roadmap
|
|
198
|
+
|
|
199
|
+
- One combined daemon (keyboard + pad + mouse in a single process).
|
|
200
|
+
- PyPI release, so `pip install elfdeckai` works without the git URL.
|
|
201
|
+
- Demo clip: the Pokรฉmon run, on camera.
|
|
202
|
+
|
|
203
|
+
## License
|
|
204
|
+
|
|
205
|
+
MIT โ see [LICENSE](LICENSE). Steal it, ship it, teach your agent to
|
|
206
|
+
speedrun something.
|
|
207
|
+
|
|
208
|
+
## Author
|
|
209
|
+
|
|
210
|
+
Created and maintained by **Garth Elfstrom**
|
|
211
|
+
([@Garthnesss](https://github.com/Garthnesss)). Issues and PRs welcome
|
|
212
|
+
on the repo.
|