This CircuitPython script transforms your Raspberry Pi Pico W or Pico 2W into a WiFi-controlled USB keyboard and mouse. Control your computer remotely by sending HTTP requests to the Pico.
- Features
- Hardware Requirements
- Installation
- Configuration
- Usage
- API Reference
- Examples
- Troubleshooting
- 🖱️ Control keyboard and mouse over WiFi
- ⌨️ Type text, press keys, and execute key combinations
- 🖱️ Move mouse, click buttons, and scroll
- 🌐 Access via mDNS hostname (
WiFi-HID.local) or IP address - 🔄 Automatic WiFi reconnection
- 📡 Support for multiple WiFi networks
- Raspberry Pi Pico W or Raspberry Pi Pico 2W
- USB cable (for power and HID connection)
- Computer with USB port
-
Download the latest CircuitPython firmware for your board:
-
Connect your Pico to your computer while holding the BOOTSEL button
-
The Pico will appear as a USB drive named
RPI-RP2 -
Drag and drop the downloaded
.uf2file onto the drive -
The Pico will reboot and appear as a drive named
CIRCUITPY
- Download the Adafruit CircuitPython Library Bundle matching your CircuitPython version (9.x, 8.x, etc.)
- Extract the bundle
- Copy the following folders/files from the
libfolder in the bundle to thelibfolder on yourCIRCUITPYdrive:adafruit_hid/(entire folder)adafruit_httpserver/(entire folder)
Your CIRCUITPY/lib folder should look like this:
CIRCUITPY/
├── lib/
│ ├── adafruit_hid/
│ │ ├── __init__.mpy
│ │ ├── consumer_control_code.mpy
│ │ ├── consumer_control.mpy
│ │ ├── keyboard_layout_base.mpy
│ │ ├── keyboard_layout_us.mpy
│ │ ├── keyboard.mpy
│ │ ├── keycode.mpy
│ │ └── mouse.mpy
│ └── adafruit_httpserver/
│ ├── __init__.mpy
│ ├── authentication.mpy
│ ├── exceptions.mpy
│ ├── headers.mpy
│ ├── interfaces.mpy
│ ├── methods.mpy
│ ├── mime_types.mpy
│ ├── request.mpy
│ ├── response.mpy
│ ├── route.mpy
│ ├── server.mpy
│ └── status.mpy
- Copy
code.pyto the root of yourCIRCUITPYdrive - Create and configure
secrets.py(see Configuration below) - The Pico will automatically restart and run the script
Create a secrets.py file in the root of your CIRCUITPY drive with your WiFi credentials.
secrets = {
'ssid': 'YourWiFiName',
'password': 'YourWiFiPassword'
}The script will try each network in order until it successfully connects:
secrets = {
# Default network (optional - will be added to the list if not already present)
'ssid': 'HomeNetwork',
'password': 'homepassword123',
# List of all networks to try
'networks': [
{'ssid': 'HomeNetwork', 'password': 'homepassword123'},
{'ssid': 'WorkNetwork', 'password': 'workpass456'},
{'ssid': 'MobileHotspot', 'password': 'mobile789'},
]
}Important Notes:
- Replace
YourWiFiNameandYourWiFiPasswordwith your actual WiFi credentials - Use single quotes for strings containing apostrophes:
'Bob\'s Network' - The file must be named exactly
secrets.py - This file contains sensitive information - keep it secure!
After the Pico boots and connects to WiFi, you can find its IP address by:
- Check the serial console: Connect via a serial terminal (9600 baud) to see startup messages
- Check your router's DHCP client list: Look for a device named
WiFi-HIDor with the Pico's MAC address - Use mDNS discovery tools: Look for
WiFi-HID.localon your network
The console output will show:
Connecting to WiFi...
Connecting to HomeNetwork...
Connected!
mDNS hostname set to: WiFi-HID.local
Starting server...
Listening on: http://WiFi-HID.local or http://192.168.1.100
You can control the device using either:
- IP Address (recommended - much faster):
http://192.168.1.100/command - mDNS Hostname:
http://WiFi-HID.local/command
Note: Using the IP address is significantly faster than using the hostname because it bypasses mDNS resolution. For best performance, use the IP address directly.
Send POST requests to the /command endpoint with JSON payloads describing the action to perform.
Endpoint: POST /command
Content-Type: application/json
| Command | Description |
|---|---|
type |
Type text as if typed on a keyboard |
key |
Press, hold, or release specific keys |
releaseAll |
Release all currently pressed keys |
mouse |
Move mouse, click buttons, or scroll |
Types text as if you were typing on a keyboard.
Parameters:
command:"type"(required)text: String to type (required)
Example:
{
"command": "type",
"text": "Hello, World!"
}Press individual keys or key combinations.
Parameters:
command:"key"(required)key: Key name (required) - see Available Keysaction:"press"(default),"keyDown", or"keyUp"(optional)
Actions:
press: Press and release immediately (default)keyDown: Press and hold the keykeyUp: Release a previously held key
Example:
{
"command": "key",
"key": "ENTER",
"action": "press"
}Releases all currently pressed keys. Useful for recovering from stuck modifier keys.
Parameters:
command:"releaseAll"(required)
Example:
{
"command": "releaseAll"
}Control mouse movement, clicks, and scrolling.
Parameters:
command:"mouse"(required)x: Horizontal movement in pixels (optional, default: 0, range: -127 to 127)y: Vertical movement in pixels (optional, default: 0, range: -127 to 127)wheel: Scroll amount (optional, default: 0, negative = scroll down, positive = scroll up)action:"click","buttonDown", or"buttonUp"(optional)button:"LEFT","RIGHT", or"MIDDLE"(required if action is specified)
Mouse Buttons:
LEFT: Left mouse buttonRIGHT: Right mouse buttonMIDDLE: Middle mouse button
Example:
{
"command": "mouse",
"x": 100,
"y": -50,
"wheel": 0,
"action": "click",
"button": "LEFT"
}The following keys are available for the key command:
Letters: A-Z
Numbers: ZERO through NINE (or 0-9)
Function Keys: F1-F24
Modifiers:
SHIFT,LEFT_SHIFT,RIGHT_SHIFTCONTROL,LEFT_CONTROL,RIGHT_CONTROLALT,LEFT_ALT,RIGHT_ALTGUI,LEFT_GUI,RIGHT_GUI(Windows key / Command key)
Navigation:
UP_ARROW,DOWN_ARROW,LEFT_ARROW,RIGHT_ARROWPAGE_UP,PAGE_DOWNHOME,END
Special Keys:
ENTER,RETURNESCAPEBACKSPACETABSPACE,SPACEBARDELETEINSERTCAPS_LOCKPRINT_SCREENSCROLL_LOCKPAUSE
Punctuation:
PERIOD,COMMAFORWARD_SLASH,BACKSLASHSEMICOLON,QUOTELEFT_BRACKET,RIGHT_BRACKETMINUS,EQUALSGRAVE_ACCENT(backtick)
(And more - check the CircuitPython HID documentation for a complete list)
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"type","text":"Hello from my Pico!"}'curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"ENTER"}'# Press Windows key down
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"GUI","action":"keyDown"}'
# Press R
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"R","action":"press"}'
# Release Windows key
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"GUI","action":"keyUp"}'# Press Ctrl down
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"CONTROL","action":"keyDown"}'
# Press C
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"C","action":"press"}'
# Release Ctrl
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"key","key":"CONTROL","action":"keyUp"}'# Move mouse 100 pixels right and 50 pixels down
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","x":100,"y":50}'curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","action":"click","button":"LEFT"}'curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","action":"click","button":"RIGHT"}'curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","wheel":-5}'# Move to start position
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","x":100,"y":100}'
# Press left button down
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","action":"buttonDown","button":"LEFT"}'
# Move while holding
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","x":200,"y":0}'
# Release button
curl -X POST http://192.168.1.100/command \
-H "Content-Type: application/json" \
-d '{"command":"mouse","action":"buttonUp","button":"LEFT"}'import requests
PICO_IP = "192.168.1.100" # Use IP for faster response
PICO_URL = f"http://{PICO_IP}/command"
def type_text(text):
response = requests.post(PICO_URL, json={"command": "type", "text": text})
return response.json()
# Usage
type_text("Hello from Python!")import requests
PICO_IP = "192.168.1.100"
PICO_URL = f"http://{PICO_IP}/command"
def press_key(key, action="press"):
response = requests.post(PICO_URL, json={
"command": "key",
"key": key.upper(),
"action": action
})
return response.json()
# Usage
press_key("ENTER")
press_key("SHIFT", "keyDown")
press_key("A", "press")
press_key("SHIFT", "keyUp")import requests
import time
PICO_IP = "192.168.1.100"
PICO_URL = f"http://{PICO_IP}/command"
def keyboard_shortcut(*keys):
"""Press a keyboard shortcut (e.g., Ctrl+C, Alt+Tab)"""
# Press all keys down
for key in keys[:-1]:
requests.post(PICO_URL, json={"command": "key", "key": key.upper(), "action": "keyDown"})
time.sleep(0.05)
# Press and release the final key
requests.post(PICO_URL, json={"command": "key", "key": keys[-1].upper(), "action": "press"})
time.sleep(0.05)
# Release modifier keys in reverse order
for key in reversed(keys[:-1]):
requests.post(PICO_URL, json={"command": "key", "key": key.upper(), "action": "keyUp"})
time.sleep(0.05)
# Usage
keyboard_shortcut("CONTROL", "C") # Ctrl+C
keyboard_shortcut("CONTROL", "SHIFT", "ESC") # Ctrl+Shift+Esc (Task Manager)
keyboard_shortcut("GUI", "L") # Windows+L (Lock screen)import requests
PICO_IP = "192.168.1.100"
PICO_URL = f"http://{PICO_IP}/command"
def move_mouse(x, y):
"""Move mouse by x, y pixels"""
response = requests.post(PICO_URL, json={
"command": "mouse",
"x": x,
"y": y
})
return response.json()
def mouse_click(button="LEFT"):
"""Click a mouse button"""
response = requests.post(PICO_URL, json={
"command": "mouse",
"action": "click",
"button": button.upper()
})
return response.json()
def mouse_scroll(amount):
"""Scroll the mouse wheel (negative = down, positive = up)"""
response = requests.post(PICO_URL, json={
"command": "mouse",
"wheel": amount
})
return response.json()
# Usage
move_mouse(100, -50)
mouse_click("LEFT")
mouse_click("RIGHT")
mouse_scroll(-3) # Scroll downimport requests
import time
PICO_IP = "192.168.1.100" # Much faster than using WiFi-HID.local
PICO_URL = f"http://{PICO_IP}/command"
def send_command(payload):
"""Send a command to the Pico"""
response = requests.post(PICO_URL, json=payload, timeout=2)
return response.json()
# Open Notepad on Windows
send_command({"command": "key", "key": "GUI", "action": "keyDown"})
time.sleep(0.1)
send_command({"command": "key", "key": "R", "action": "press"})
time.sleep(0.1)
send_command({"command": "key", "key": "GUI", "action": "keyUp"})
time.sleep(0.5)
# Type "notepad" and press Enter
send_command({"command": "type", "text": "notepad"})
time.sleep(0.2)
send_command({"command": "key", "key": "ENTER"})
time.sleep(1)
# Type some text
send_command({"command": "type", "text": "This is automated via WiFi HID!\n"})
send_command({"command": "type", "text": "Pretty cool, right?"})const axios = require('axios');
const PICO_IP = '192.168.1.100'; // IP address is faster!
const PICO_URL = `http://${PICO_IP}/command`;
async function typeText(text) {
const response = await axios.post(PICO_URL, {
command: 'type',
text: text
});
return response.data;
}
async function pressKey(key, action = 'press') {
const response = await axios.post(PICO_URL, {
command: 'key',
key: key.toUpperCase(),
action: action
});
return response.data;
}
async function moveMouse(x, y) {
const response = await axios.post(PICO_URL, {
command: 'mouse',
x: x,
y: y
});
return response.data;
}
async function mouseClick(button = 'LEFT') {
const response = await axios.post(PICO_URL, {
command: 'mouse',
action: 'click',
button: button.toUpperCase()
});
return response.data;
}
// Usage
(async () => {
await typeText('Hello from JavaScript!');
await pressKey('ENTER');
await moveMouse(100, 100);
await mouseClick('LEFT');
})();# Set variables
$PicoIP = "192.168.1.100" # Faster than hostname
$PicoURL = "http://$PicoIP/command"
# Type text
$body = @{
command = "type"
text = "Hello from PowerShell!"
} | ConvertTo-Json
Invoke-RestMethod -Uri $PicoURL -Method Post -Body $body -ContentType "application/json"
# Press Enter
$body = @{
command = "key"
key = "ENTER"
} | ConvertTo-Json
Invoke-RestMethod -Uri $PicoURL -Method Post -Body $body -ContentType "application/json"
# Move mouse
$body = @{
command = "mouse"
x = 100
y = 50
} | ConvertTo-Json
Invoke-RestMethod -Uri $PicoURL -Method Post -Body $body -ContentType "application/json"
# Click left mouse button
$body = @{
command = "mouse"
action = "click"
button = "LEFT"
} | ConvertTo-Json
Invoke-RestMethod -Uri $PicoURL -Method Post -Body $body -ContentType "application/json"For the best performance, always use the IP address instead of the mDNS hostname (WiFi-HID.local).
Why? mDNS resolution adds latency to every request. Using the IP address directly can be 50-200ms faster per request, which is significant for real-time control.
Slow:
PICO_URL = "http://WiFi-HID.local/command" # mDNS lookup on every requestFast:
PICO_URL = "http://192.168.1.100/command" # Direct connectionFor complex operations, send commands in quick succession rather than waiting for responses:
import requests
from concurrent.futures import ThreadPoolExecutor
PICO_URL = "http://192.168.1.100/command"
commands = [
{"command": "type", "text": "Hello "},
{"command": "type", "text": "World"},
{"command": "key", "key": "ENTER"},
]
# Send all commands quickly
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(executor.map(lambda cmd: requests.post(PICO_URL, json=cmd), commands))- Check your
secrets.pyfile for typos in SSID or password - Ensure your WiFi is 2.4GHz (Pico W doesn't support 5GHz)
- Check the serial console for error messages
- Try resetting the Pico by unplugging and reconnecting it
- Ensure your computer supports mDNS:
- Windows: Install Bonjour Print Services or iTunes
- macOS/Linux: Built-in support
- Try using the IP address directly instead (faster anyway!)
- Check firewall settings
- Ensure the Pico is enumerated as a USB HID device
- Try unplugging and reconnecting the USB cable
- Check that the key name matches the available keys (case-insensitive)
- Use
releaseAllto reset stuck keys
- Verify the Pico is recognized as a USB mouse
- Remember: x and y values are relative movements (range: -127 to 127)
- For large movements, send multiple commands
- Check the serial console for errors
- Verify the Pico is still connected to WiFi
- Try pinging the IP address
- Reset the Pico
- Use the IP address instead of
WiFi-HID.localhostname - Reduce WiFi interference
- Move the Pico closer to your WiFi router
- Check network congestion
Modify code.py to set a static IP (add after WiFi connection):
import ipaddress
wifi.radio.set_ipv4_address(
ipv4=ipaddress.IPv4Address("192.168.1.50"),
netmask=ipaddress.IPv4Address("255.255.255.0"),
gateway=ipaddress.IPv4Address("192.168.1.1"),
dns=ipaddress.IPv4Address("8.8.8.8")
)Change the mDNS hostname by modifying this line in code.py:
mdns_server.hostname = "WiFi-HID" # Change to your preferred nameThis script has no authentication by default. Anyone on your network can send commands. For security:
- Use on trusted networks only
- Consider implementing authentication in the HTTP handler
- Use a firewall to restrict access
- Don't expose to the internet
This project uses CircuitPython and Adafruit libraries. Please refer to their respective licenses.
For issues related to:
- CircuitPython: CircuitPython GitHub
- Adafruit Libraries: Adafruit CircuitPython Bundle
- This Script: Check the code comments and this documentation
Happy automating! 🚀