DeskDeck is a compact 10-key Stream Deck-style controller built from an Arduino Mega 2560 and a Windows desktop configuration app.
The Arduino firmware only reports physical button state over USB Serial. The desktop app owns all higher-level behavior: serial connection, profiles, key assignment, action execution, logs, and tray/background operation.
Implemented:
- 10 physical mechanical switches
- Arduino Mega 2560 firmware
- USB Serial protocol at 115200 baud
- Windows desktop configuration app
- 2 x 5 key layout in the GUI
- One configurable action per key
- Profiles
- Logs
- Auto-connect to the Arduino serial port
- System tray/background mode
- Windows logon auto-start through the current user's Run registry key
- Two 16 x 2 I2C LCDs
- Three LCD navigation buttons
- One potentiometer on A0
- Local Windows PC telemetry on the LCD menu
The device uses 10 two-pin mechanical switches. One leg of every switch is connected to a shared GND rail. The other leg of each switch is connected directly to a digital pin on the Arduino Mega 2560.
No external pull-up or pull-down resistors are used. The firmware enables the Arduino internal pull-up resistors with INPUT_PULLUP.
Because of INPUT_PULLUP:
- Released button reads
HIGH - Pressed button reads
LOW
| Physical Key | GUI Label | Arduino ID | Arduino Mega Pin |
|---|---|---|---|
| Key 1 | 1 |
1 | D22 |
| Key 2 | 2 |
2 | D23 |
| Key 3 | 3 |
3 | D24 |
| Key 4 | 4 |
4 | D25 |
| Key 5 | 5 |
5 | D26 |
| Key 6 | 6 |
6 | D27 |
| Key 7 | 7 |
7 | D28 |
| Key 8 | 8 |
8 | D29 |
| Key 9 | 9 |
9 | D30 |
| Key 10 | 0 |
10 | D31 |
Key 10 is displayed as 0 in the desktop app, but its config key and Arduino ID remain 10.
Additional controls:
| Control | Arduino Mega Pin |
|---|---|
| SELECT | D32 |
| BACK | D33 |
| MODE | D34 |
| Potentiometer wiper | A0 |
The navigation buttons are wired to GND and use INPUT_PULLUP, so pressed reads LOW.
The top LCD uses the Mega hardware I2C bus. The bottom LCD uses a separate software I2C bus so both LCD backpacks can keep the same 0x27 address.
| Display | I2C Address | Arduino Mega Pins |
|---|---|---|
| Top LCD | 0x27 |
SDA D20, SCL D21 |
| Bottom LCD | 0x27 |
SDA D53, SCL D52 |
1 2 3 4 5
6 7 8 9 0
The Arduino opens Serial at 115200 baud and prints the startup banner:
STREAM_DECK_READY
BUTTON_COUNT=10
Macro button events are newline-delimited JSON:
{"type":"button","id":1,"action":"pressed"}
{"type":"button","id":1,"action":"released"}The firmware does not send action names, file paths, URLs, commands, hotkeys, or macro data. The desktop app only accepts button ID and physical state from Arduino. Actions are loaded from the local JSON config file.
Control button events are also physical-state-only JSON:
{"type":"control","control":"select","action":"pressed"}
{"type":"control","control":"select","action":"released"}Potentiometer page changes are emitted only when the filtered A0 value crosses a menu page boundary:
{"type":"menu","control":"pot","action":"page_changed","page":2,"raw":348,"normalized":0.340}The desktop app sends LCD frames and notifications to Arduino:
{"type":"display_frame","top":{"line1":"CPU 42% 58C","line2":"RAM 9.2 / 32G"},"bottom":{"line1":"> CPU Details","line2":"Page 2/7 LIVE"}}This release monitors only the Windows computer running DeskDeck. Homeserver integration is not included.
The desktop app owns the menu state. Arduino only reports physical input and renders display commands.
Pages:
0 Overview
1 CPU
2 GPU
3 Memory
4 Disk
5 Network
6 System
Controls:
- SELECT short press: toggles detail mode.
- SELECT long press: toggles pinned mode.
- BACK short press: returns to Overview.
- BACK long press: resets menu state.
- MODE short press: toggles auto rotate.
- MODE long press: toggles LCD backlight.
Auto rotate advances every 5 seconds through Overview, CPU, GPU, Memory, Disk, Network, and System. Pinned mode blocks potentiometer page changes and auto rotate.
Startup LCD text before the first desktop frame:
Top:
DeskDeck v0.2
Waiting for PC
Bottom:
USB Serial Ready
Page: Overview
Each key has one action. The action runs when the desktop app receives a physical pressed event from Arduino.
Mouse-clicking a keycap in the GUI only selects that key for editing. It does not run the assigned action.
Supported action types:
nonelaunchurlhotkeycommand
command actions are executed with subprocess.Popen([...], shell=False). Program and arguments are stored separately.
deskdeck/
|-- README.md
|-- platformio.ini
|-- src/
| `-- main.cpp
|-- include/
| `-- README
|-- lib/
| `-- README
|-- test/
| `-- README
`-- desktop-agent/
|-- main.py
|-- requirements.txt
|-- README.md
|-- config.example.json
|-- pytest.ini
|-- deskdeck/
| |-- application.py
| |-- main_window.py
| |-- constants.py
| |-- theme.py
| |-- models.py
| |-- config_manager.py
| |-- serial_worker.py
| |-- serial_controller.py
| |-- action_executor.py
| |-- button_controller.py
| |-- profile_manager.py
| |-- logger.py
| |-- pages/
| `-- widgets/
`-- tests/
The firmware lives in src/main.cpp and is built with PlatformIO.
Responsibilities:
- Configure D22-D31 as
INPUT_PULLUP - Read 10 buttons
- Configure D32-D34 as
INPUT_PULLUPnavigation buttons - Read the A0 potentiometer
- Drive
topDisplayon hardware I2C at0x27 - Drive
bottomDisplayon software I2C SDA D53 / SCL D52 at0x27 - Debounce every button independently
- Use
millis()for non-blocking timing - Print startup status
- Print pressed/released JSON events
- Receive local PC telemetry JSON and render the LCD menu
The firmware deliberately does not know about profiles or actions.
pio runpio run --target uploadpio device monitor --baud 115200Close the serial monitor before connecting from the desktop app. A COM port can only be opened by one process at a time.
The desktop app lives in desktop-agent/.
Responsibilities:
- Detect serial ports
- Connect to the Arduino at 115200 baud
- Parse startup lines and JSON button events
- Collect local Windows CPU, memory, disk, and uptime telemetry
- Send telemetry JSON to the Arduino LCD menu
- Show live key press state
- Store profiles in
config.json - Run local actions for physical button presses
- Show logs
- Run in the Windows system tray
- Auto-start at Windows logon
cd desktop-agent
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txtpython main.pypython main.py --traypytestDeskDeck is configured to auto-start for the current Windows user by adding a value under:
HKCU\Software\Microsoft\Windows\CurrentVersion\Run
The command starts the GUI without opening the main window:
pythonw.exe main.py --tray
This is intentionally not a Windows Service. Windows Services run in session 0 and are not suitable for tray icons or interactive GUI windows.
Default config path:
desktop-agent/config.json
config.json is generated on first run and ignored by Git because it contains local COM port selections and user-specific application paths. The repository includes desktop-agent/config.example.json as a clean template.
Example button config:
{
"name": "Discord",
"action": {
"type": "launch",
"path": "C:/Users/Administrator/AppData/Local/Discord/Update.exe",
"args": ["--processStart", "Discord.exe"]
}
}The config file is saved atomically: DeskDeck writes a temporary file first, then replaces the real config file.
- Close Arduino Serial Monitor, PlatformIO monitor, or any program using the same COM port.
- Open the
Connectionpage and clickRefresh Ports. - Confirm the selected COM port matches the Arduino in Device Manager.
- Check the
Logspage for the exact serial error.
- Confirm the Arduino sends a
pressedJSON line for that key. - Check that the selected profile has an action configured for that key.
- Check
Logsfor action validation or launch errors.
Discord usually needs to be launched through Update.exe:
{
"type": "launch",
"path": "C:/Users/Administrator/AppData/Local/Discord/Update.exe",
"args": ["--processStart", "Discord.exe"]
}DeskDeck may be hidden in the system tray. Click the DeskDeck tray icon or use the tray context menu and select Open DeskDeck.