A bare metal shell OS, currently compatible for RP2040.
ESP32 port also available — see DeckOS_ESP32 for the ESP32 (ESP-IDF / FreeRTOS) port with native WiFi, ESP-NOW, camera support, NRF24L01 radio, and SPIFFS file persistence. See the ESP32 Port section below for details.
- What you need
- How it's structured
- Getting started
- Using the shell
- Commands
- WiFi / ESP8266
- MQTT
- Swarm / ESP-NOW
- Bluetooth / HC-05
- Filesystem
- Modules (on-demand RAM)
- USB portable OS
- DeckScript
- Buzzer setup
- Config system
- Syslog
- Scheduler
- Boot modes
- Drivers
- ESP32 Port
- Demo
| Thing | Detail |
|---|---|
| Board | Raspberry Pi Pico (RP2040) |
| Connection | USB to your computer |
| Optional | Passive buzzer on any GPIO pin (for tone / melody / piano) |
| Optional | I²C device on GP4 (SDA) and GP5 (SCL) |
| Optional | SPI device; default pins GP2 (SCK), GP3 (MOSI), GP4 (MISO) |
| Optional | Servo on any GPIO pin |
| Optional | ESP8266 module on UART1 (GP5 TX, GP4 RX) for WiFi |
| Optional | HC-05 Bluetooth module on UART0 for wireless shell |
┌─────────────────────────────────────────────┐
│ Core 0 │
│ kernel_init() → shell_run() (main loop) │
│ commands, I/O, user interaction │
└────────────────────┬────────────────────────┘
│ multicore_launch_core1()
┌────────────────────▼────────────────────────┐
│ Core 1 │
│ scheduler → background tasks & jobs │
│ (servo sweeps, la trigger capture, ...) │
└─────────────────────────────────────────────┘
Boot order:
kernel_init()
├── syslog_init()
├── bootloader_run() (mode detect, config load, banner)
├── drivers_init_all() (adc, gpio, pwm, i2c0)
├── sched_init() (launches Core 1)
└── shell_init() (registers commands, prints prompt)
kernel_run() [infinite loop]
└── shell_run() (non-blocking input → parse → dispatch)
- Raspberry Pi Pico SDK v3.0 or newer
- CMake 3.13+
- ARM GCC toolchain (
arm-none-eabi-gcc)
git clone https://github.com/enginestein/DeckOS
cd DeckOS
make
make flash# Linux / macOS
minicom -b 115200 -D /dev/ttyACM0
# Windows - PuTTY or Tera Term, COMx, 115200 8N1
# Or simply:
make monitorYou'll see the boot banner and a > prompt. You're in.
> help ← list everything
> sysinfo ← board summary
> temp ← core temperature
> led blink 3 ← blink the LED 3 times
> gpio read 15 ← read GP15
| Key | What it does |
|---|---|
↑ / ↓ |
Browse command history (last 16 commands; see also the history command) |
Backspace |
Delete a character |
Ctrl-C |
Cancel what you're typing |
Ctrl-D |
Quick shortcut for uptime |
Ctrl-L |
Clear the screen |
| Command | Usage | What it does |
|---|---|---|
help |
help |
List all command groups; help <group> to list a specific group |
version |
version |
Show OS version and build date |
clear |
clear |
Clear the screen |
echo |
echo <text> |
Print text back |
uptime |
uptime |
Time since last boot |
sysinfo |
sysinfo |
Board, CPU, RAM, temp, uptime -- everything at once |
stats |
stats |
Command counts, CPU speed, temperature |
top |
top |
Live task monitor showing Core 1 CPU usage per task; press any key to exit |
jobs |
jobs |
List all active background Core 1 jobs |
jobs |
jobs cancel <id> |
Cancel a specific background job |
| Command | Usage | What it does |
|---|---|---|
temp |
temp |
Internal chip temperature in °C and °F |
mem |
mem |
Available heap and flash sizes |
memmap |
memmap |
Full memory map with SRAM sections, stack, heap, and peripheral addresses |
free |
free |
Heap allocator stats and a list of every live allocation |
stack |
stack |
Core 0 stack depth, current usage percentage, free headroom, and canary high-water mark |
power |
power |
VSYS voltage via ADC3 (GP29), source detection, and LiPo / AA battery percentage estimate |
led |
led <on|off|toggle|blink [n]> |
Control the onboard LED (GP25). blink n blinks it n times |
gpio |
gpio <read|write|mode|irq> <pin> [val] |
Read, write, set direction, or monitor a GPIO pin |
pwm |
pwm <pin> <duty 0–100> [freq_hz] |
Set PWM duty cycle on any pin; optional frequency argument |
adc |
adc <0|1|2> |
Read ADC channel 0–2 (GP26–28), shows raw value and voltage |
avg |
avg <ch> [samples] |
Same as adc but averages multiple samples; shows min, max, and peak-to-peak noise |
pull |
pull <pin> <up|down|none> |
Set a pin's pull resistor |
clock |
clock [mhz] |
Get or set CPU speed (48–200 MHz) |
pin |
pin |
Snapshot of all GPIO pin states (direction and value) |
pinout |
pinout |
ASCII Pico pinout diagram with live pin states, directions, and functions |
uid |
uid |
Print the board's unique 64-bit ID |
wdog |
wdog |
Check if the last reboot was caused by the watchdog |
i2c |
i2c scan [sda scl] | read <addr> <reg> | write <addr> <reg> <val> | dump <addr> |
I²C bus tools (default GP4=SDA, GP5=SCL) |
spi |
spi init | write | read | xfer |
SPI bus operations (see below) |
uart |
uart <baud> <tx_pin> <rx_pin> [timeout_s] |
Bridge USB-CDC to a hardware UART; press Ctrl-X to exit |
flash |
flash read | write | erase <addr> |
Raw flash read/write/erase (see below) |
stack reads the current stack pointer directly and compares it to the linker-defined __StackTop / __StackLimit symbols. It also walks upward from __StackLimit looking for a 0xDEADBEEF canary pattern to report the peak usage high-water mark.
> stack
stack usage (Core 0):
top : 0x20041C00
limit : 0x20040000
size : 7168 B (7 KB)
SP now : 0x20041800
used : 1024 B (14.3%)
free : 6144 B
peak used : unknown (stack not pre-filled with canary)power reads ADC channel 3 (GP29, the internal VSYS ÷ 3 tap) and back-calculates the supply voltage. It detects USB power, a 1S LiPo, or 3× AA cells and shows a percentage estimate where applicable.
> powerGP29 must not be driven externally when using this command.
> gpio irq 15 # watch GP15 for edges (30 s default)
> gpio irq 15 60 # watch for up to 60 s
> gpio irq 15 dump # print everything captured so far
> gpio irq 15 stop # stop watchingDefault pins are GP4 (SDA) and GP5 (SCL). All subcommands accept optional [sda] [scl] pin overrides.
> i2c scan # scan on default pins
> i2c scan 6 7 # scan on GP6/GP7
> i2c read 0x68 0x75 # read one byte
> i2c write 0x68 0x6B 0x00 # write one byte
> i2c dump 0x68 # dump all 256 registersSPI must be initialised before use. Default pins are GP2 (SCK), GP3 (MOSI), GP4 (MISO) at 1 MHz.
> spi init # default pins and baud
> spi init 2 3 4 500000 # custom: sck mosi miso baud
> spi write <cs_pin> <hex bytes> # write bytes to a device
> spi read <cs_pin> <reg_hex> [n] # read n bytes from a register
> spi xfer <cs_pin> <hex bytes> # full-duplex transfer, prints RXBridges the USB serial port to a hardware UART. Useful for talking to other devices from the DeckOS shell.
> uart 9600 0 1 # 9600 baud, TX=GP0, RX=GP1
> uart 115200 4 5 30 # 115200 baud, TX=GP4, RX=GP5, 30s timeoutUART0 TX pins: GP0, GP12, GP16. UART1 TX pins: GP4, GP8. Press Ctrl-X to exit.
Addresses are XIP offsets from 0x00000000 (start of flash). The config sector is the last 4 KB of flash (e.g. 0x1FF000 on a 2 MB board, higher on larger parts) -- avoid erasing it manually. Run flash with no arguments to see the exact address detected for your board.
> flash read 10000 64 # dump 64 bytes starting at offset 0x10000
> flash erase 10000 # erase the 4 KB sector containing 0x10000
> flash write 10000 DE AD # write bytes (address must be 256-byte page-aligned)| Command | Usage | What it does |
|---|---|---|
la |
la <pin> [samples] [us_per_sample] [trigger] |
Logic analyser -- sample a pin and render an ASCII timing diagram |
scope |
scope <pin> <expected_hz> <duration_ms> |
Clean waveform viewer; auto-selects sample rate from expected frequency |
detect |
detect [sda scl] | uart <pin> [timeout_s] | analyze <pin> [samples] [us] |
Scan I²C bus, probe for UART baud rate, or guess a protocol from logic samples |
imu |
imu <read|stream|attitude|calibrate|raw|whoami> |
MPU6050 accelerometer/gyro on I²C (GP4/GP5) |
> la 15 # 128 samples at 10 µs each
> la 15 256 5 # 256 samples at 5 µs each
> la 15 256 2 trigger # background: wait for falling edge then capture
> jobs # check trigger status
> jobs cancel 0 # abort if neededAfter sampling, la prints a waveform diagram followed by edge count, duty cycle, window length, and an estimated frequency. With trigger, the capture runs on Core 1 as a background job -- the shell stays responsive while it waits.
scope auto-calculates the sample interval from your expected frequency (targeting ~5 samples per cycle) and caps the sample count at 512. It renders a clean two-line waveform with a time-axis ruler below.
> scope 15 1000 50 # view a ~1 kHz signal for 50 ms
> scope 15 50 500 # view a ~50 Hz signal for 500 ms> detect # scan I²C and report known devices
> detect 6 7 # scan on custom SDA/SCL pins
> detect uart 1 # probe GP1 for UART activity
> detect uart 1 5 # same, 5 second timeout
> detect analyze 15 # sample GP15 and guess the protocol
> detect analyze 15 256 5 # 256 samples at 5 µs each| Command | Usage | What it does |
|---|---|---|
oled init |
oled init |
Initialise display on GP4 (SDA) / GP5 (SCL) |
oled on / off |
oled on |
Power display on or off |
oled clear |
oled clear |
Blank framebuffer and flush |
oled fill |
oled fill <hex> |
Fill framebuffer with a pattern byte (e.g. AA) and flush |
oled flush |
oled flush |
Push framebuffer to screen |
oled contrast |
oled contrast <0–255> |
Set brightness |
oled invert |
oled invert <0|1> |
Invert display colours |
oled flip |
oled flip <h:0|1> <v:0|1> |
Mirror display horizontally and/or vertically |
oled text |
oled text <col> <row> <str> |
Draw text at a character-grid cell |
oled textxy |
oled textxy <x> <y> <str> |
Draw text at pixel coordinates |
oled printf |
oled printf <col> <row> <fmt...> |
Formatted text at a grid cell |
oled pixel |
oled pixel <x> <y> <0|1> |
Set or clear a single pixel |
oled line |
oled line <x0> <y0> <x1> <y1> |
Draw a line |
oled hline |
oled hline <x0> <x1> <y> |
Horizontal line |
oled vline |
oled vline <x> <y0> <y1> |
Vertical line |
oled rect |
oled rect <x> <y> <w> <h> |
Rectangle outline |
oled rectfill |
oled rectfill <x> <y> <w> <h> |
Filled rectangle |
oled circle |
oled circle <cx> <cy> <r> |
Circle outline |
oled circlefill |
oled circlefill <cx> <cy> <r> |
Filled circle |
oled progress |
oled progress <x> <y> <w> <h> <%> |
Progress bar |
oled title |
oled title <text> |
Title bar at row 0 (inverted) |
oled status |
oled status <left> <right> |
Status bar on bottom row |
oled splash |
oled splash <line1> <line2> |
Splash screen and flush |
oled notify |
oled notify <msg> <ms> |
Timed notification overlay (100–30000 ms) |
oled scroll |
oled scroll <right|left> <sp> <ep> |
Hardware scroll between pages sp–ep |
oled scroll stop |
oled scroll stop |
Stop hardware scroll |
oled spinner |
oled spinner <x> <y> <frame 0–7> |
Spinner glyph at pixel position |
oled boot |
oled boot |
Animated boot sequence |
| Command | Usage | What it does |
|---|---|---|
servo |
servo <pin> <angle 0–180> |
Move a servo to an absolute angle |
servo sweep |
servo sweep <pin> [from to step_ms] |
Blocking sweep from one angle to another |
servo bg |
servo bg <pin> sweep [min max step step_ms] |
Background sweep (Core 1), non-blocking |
servo bg |
servo bg <pin> goto <angle> [step_ms] |
Move to angle in background |
servo bg |
servo bg <pin> stop |
Stop a background servo |
servo bg list |
servo bg list |
List all active background servos |
> servo 16 90 # centre a servo on GP16
> servo sweep 16 0 180 20 # sweep 0→180° with 20 ms steps
> servo bg 16 sweep 0 180 1 15 # background sweep, 1° steps every 15 ms
> servo bg 16 goto 45 # move to 45° in background
> servo bg 16 stop| Command | Usage | What it does |
|---|---|---|
tone |
tone <pin> <note|hz> [ms] |
Play a tone on a buzzer -- use a note name like C4 or A#3, or a raw Hz value |
melody |
melody <pin> <C4:200 E4:200 ...> |
Play a sequence of notes. Format is NOTE:duration_ms; use REST for silence |
melody |
melody <pin> elise |
Play the full Für Elise arrangement |
melody |
melody <pin> canon |
Play the full Canon in D arrangement |
morse |
morse <text> [wpm] |
Blink the onboard LED in morse code (default 13 WPM) |
piano |
piano <pin> [duration_ms] |
Interactive keyboard piano -- drive a buzzer from keypresses |
> tone 16 A4 500
> tone 16 440 500
> melody 16 C4:200 E4:200 G4:400 REST:100 C5:600
> melody 16 elise
> melody 16 canon
> morse SOS
> morse HELLO 20
> piano 16
> piano 16 200 # 200 ms per noteBlack keys: W E T Y U O P (sharps/flats)
White keys: A S D F G H J K L ;
[ and ] shift the base octave down and up. Press q to quit.
| Command | Usage | What it does |
|---|---|---|
sleep |
sleep <ms> |
Wait for a number of milliseconds (1–30000) |
repeat |
repeat <n> <command> |
Run a command n times in a row (1–100) |
watch |
watch <ms> <command> |
Run a command repeatedly at an interval; press any key to stop |
trigger |
trigger <pin> <rise|fall|both> <command> |
Watch a pin and run a command the moment an edge fires (one-shot) |
cron |
cron <delay_ms> <command> |
Schedule a command to run once after a delay; returns immediately |
bench |
bench <iterations> <command> |
Run a command many times and report throughput and timing |
time |
time <command> |
Run a command and report how long it took (ms and µs) |
alias |
alias [name [command...]] |
Define, list, or show command aliases |
unalias |
unalias <name> |
Remove an alias |
run |
run <file> |
Run a DeckScript file from VFS |
script |
script run <file> |
Same as run |
script |
script test |
Run the built-in DeckScript self-test |
edit |
edit <file> |
Open a file in the built-in nano-style text editor |
> watch 1000 temp
> watch 500 adc 0
> trigger 15 fall led on
> repeat 5 led toggle
> cron 5000 reboot
> bench 1000 echo hi
> time sysinfo
> run /home/blink.ds
> edit /home/blink.dsAliases let you give a short name to a longer command. The alias name is replaced
by its value, and any extra arguments you type are appended. Expansion happens
exactly once, so a self-referential alias such as alias ls "ls -l" is safe
and will not loop.
> alias ll ls # 'll' now runs 'ls'
> alias t temp # 't' now runs 'temp'
> ll /home # expands to: ls /home
> alias # list all defined aliases
> alias ll # show one alias
> unalias ll # remove itUp to 16 aliases can be defined. Aliases live in RAM and are cleared on reboot.
| Command | Usage | What it does |
|---|---|---|
reboot |
reboot |
Save VFS to flash then reboot via watchdog after 1 second |
dfu |
dfu |
Jump into USB bootloader mode for reflashing |
uid |
uid |
Print the board's unique 64-bit ID |
wdog |
wdog |
Check if the last reboot was caused by the watchdog |
fault |
fault info |
Show CPU registers captured from the last HardFault this session |
fault |
fault test |
Trigger a deliberate fault to verify the handler and syslog integration |
date |
date |
Show the current date and time from the RP2040 RTC |
date |
date set <Y> <M> <D> <h> <m> <s> |
Set the real-time clock |
history |
history |
List the command history (last 16 commands) |
history |
history clear |
Clear the command history |
uname |
uname [-a] |
Print the OS name; -a shows full system identity |
rand |
rand [min] [max] |
Generate a hardware random number |
The HardFault handler is installed automatically at boot. It captures PC, LR, SP, R0, R12, and xPSR, writes them to syslog, then reboots via watchdog. After an unexpected reboot, run syslog err to see the cause.
> fault info
> fault test # board will reboot; check 'syslog err' afterdate uses the RP2040's built-in hardware RTC. The clock starts at
2025-01-01 00:00:00 on boot and keeps ticking while the board is powered. Set
it once per session to get accurate timestamps. The day of the week is computed
automatically.
> date # show current time
> date set 2026 5 30 19 45 00 # YYYY MM DD hh mm ssThe RP2040 RTC is not battery-backed -- the time resets on every power loss.
rand draws from the RP2040's hardware ring-oscillator entropy source, so the
values are genuinely random (unlike a seeded software PRNG).
> rand # full 32-bit value
> rand 6 # 0 .. 5 (e.g. a die index)
> rand 1 6 # 1 .. 6 inclusive> uname # -> DeckOS
> uname -a # DeckOS 5.0 <hostname> RP2040 Cortex-M0+ <mhz> SRAM=.. flash=.. <board>| Command | Usage | What it does |
|---|---|---|
drivers |
drivers |
Show all drivers and whether they initialised cleanly |
tasks |
tasks [enable|disable <id>] |
View or toggle background scheduler tasks |
config |
config show|set <key> <val>|save|reset |
Manage persistent settings |
syslog |
syslog show [n]|warn|err|write <tag> <msg>|clear|stats |
Read and manage the in-memory log |
Config keys you can set:
| Key | Values | Effect |
|---|---|---|
hostname |
any string | Name shown at boot |
cpu_mhz |
48–200, or 0 for default 125 | CPU speed set at boot |
boot_led |
0 or 1 | Whether the LED turns on at boot |
> config set hostname my-pico
> config set cpu_mhz 133
> config set boot_led 1
> config save
> config show
> config resetDeckOS supports WiFi through an ESP8266 module running the DeckOS Bridge firmware -- a small Arduino sketch that lives on the ESP8266 and translates DeckOS commands into WiFi actions. This lets the Pico connect to networks, make HTTP requests, run a telnet server, and more, all from the DeckOS shell.
- Scan nearby networks -- see every access point in range with signal strength and security type
- Connect to WiFi -- join a network so the ESP8266 has an IP address
- HTTP GET and POST -- pull data from or push data to any URL
- Telnet server -- accept incoming terminal connections on port 23
- HTTP server -- turn the Pico into a tiny web endpoint on port 80
- Remote monitoring -- push sensor readings to a server without a USB cable near the Pico
- Wireless data logging -- log GPIO events, ADC samples, or syslog entries to a remote server
- Headless deployment -- once configured, the Pico can operate without USB and still participate in your network
| ESP8266 pin | Pico pin | Notes |
|---|---|---|
| TX | GP5 | |
| RX | GP4 | |
| VCC / 3V3 | External 3.3V | Use a dedicated regulator; the Pico's 3V3 pin may brown out under WiFi load |
| GND | GND | Common ground required |
| EN / CH_PD | 3.3V | Must be HIGH for the module to boot |
Flash ESP8266_DeckOS_Bridge.ino to the ESP8266 using the Arduino IDE with the ESP8266 board package installed. Before flashing, edit the credentials at the top of the sketch:
String wifi_ssid = "YourNetworkName";
String wifi_password = "YourPassword";The bridge starts in auto-detect mode and responds to @-prefixed control commands from the Pico.
| Command | What it does |
|---|---|
wifi init |
Initialise the ESP8266 on UART1 at 115200 baud |
wifi init <baud> |
Initialise at a custom baud rate |
wifi status |
Show UART config and ready state |
wifi ping |
Query the bridge and show its current status |
wifi scan |
Scan for WiFi networks |
wifi join <ssid> <pass> |
Join a network |
wifi ip |
Show the assigned IP address |
wifi shell |
Drop into a raw interactive shell with the ESP8266 |
wifi deinit |
Release UART1 from the ESP8266 |
wifi get <url> |
HTTP GET request |
wifi post <url> <body> |
HTTP POST request |
wifi serve |
Start an HTTP server on port 80 |
wifi telnet |
Start a telnet server on port 23 |
wifi telnet stop |
Stop the telnet server |
wifi bridge status |
Show bridge mode, WiFi connection state, and IP address |
wifi bridge scan |
Scan for nearby WiFi networks via bridge |
wifi bridge connect |
Connect using credentials stored in the bridge firmware |
wifi bridge reset |
Reboot the ESP8266 |
wifi bridge auto |
Switch bridge to auto-detect firmware mode |
wifi bridge at |
Switch bridge to raw AT command passthrough |
wifi bridge raw |
Switch bridge to raw command mode |
> wifi init # start the UART link to the ESP8266
> wifi bridge status # confirm the bridge is alive and check WiFi state
> wifi bridge scan # see what networks are in range
> wifi bridge connect # connect using credentials in the bridge firmware
> wifi bridge status # confirm connection and get the IP address
> wifi get http://example.com/data
> wifi post http://example.com/log "temp=27.3"| Command | What it does |
|---|---|
mqtt server <host> |
Set broker address |
mqtt port <n> |
Set broker port (default 1883) |
mqtt id <name> |
Set client ID |
mqtt connect |
Connect to broker |
mqtt disconnect |
Disconnect from broker |
mqtt status |
Show connection state |
mqtt pub <topic> <msg> |
Publish a message |
mqtt sub <topic> |
Subscribe to a topic |
mqtt unsub <topic> |
Unsubscribe from a topic |
Requires WiFi to be initialised and connected first.
> wifi init
> wifi bridge connect
> mqtt server 192.168.1.100
> mqtt connect
> mqtt pub deck/temp 23.4
> mqtt sub deck/cmd/#Lets multiple Picos communicate peer-to-peer without a router, useful for drone swarms or sensor meshes.
| Command | What it does |
|---|---|
swarm init |
Start ESP-NOW mesh |
swarm id <name> |
Set this node's name |
swarm mac |
Show this node's MAC address |
swarm peer <MAC> |
Register a peer node |
swarm pub <lat> <lon> <alt> <hdg> <state> |
Broadcast position telemetry |
swarm list |
Show all known peers |
swarm stop |
Stop the mesh |
# Node 1
> wifi init
> swarm init
> swarm mac # note MAC: AA:BB:CC:DD:EE:01
> swarm id drone1
# Node 2
> wifi init
> swarm init
> swarm mac # note MAC: AA:BB:CC:DD:EE:02
> swarm id drone2
> swarm peer AA:BB:CC:DD:EE:01 # register node 1
# Broadcast from node 1
> swarm pub 28.6139 77.2090 100.0 180.0 1DeckOS supports wireless shell access through an HC-05 Bluetooth module. Once paired, you can control the Pico from a phone or laptop terminal app without touching a USB cable.
- Wireless shell -- run any DeckOS command from a Bluetooth terminal on your phone or laptop
- Remote command execution -- send a single command from the BT side and get the output back
- Live stats streaming -- stream
top-style CPU and temperature data to a connected Bluetooth client - Syslog mirroring -- forward every log entry to the Bluetooth terminal as it happens
- File transfer -- send and receive VFS files over the Bluetooth link
HC-05 VCC → VSYS / 5V / VBUS
HC-05 GND → GND
HC-05 RXD → GP1
HC-05 TXD → GP0
| Command | What it does |
|---|---|
bt init [baud] |
Initialise the HC-05 UART |
bt status |
Show init state, connection state, log mirror state |
bt shell |
Start a full wireless DeckOS terminal over Bluetooth |
bt exec <command> |
Run one command and send the output to the BT client |
bt top [ms] |
Stream live CPU/temp stats to the BT client |
bt log on |
Mirror all syslog entries to the BT client in real time |
bt log off |
Stop mirroring |
bt send <file> |
Send a VFS file over Bluetooth |
bt recv <file> |
Receive a file from Bluetooth into VFS |
bt sniff [s] |
Raw byte sniffer -- prints hex and ASCII |
bt at |
Drop into interactive AT command mode |
bt name <name> |
Set the HC-05 module name (requires AT mode) |
bt pin <code> |
Set the pairing PIN (requires AT mode) |
bt baud <rate> |
Change the HC-05 UART baud rate (requires AT mode) |
> bt init
> bt status
> bt shell # connect from your phone nowDeckOS includes a small in-memory virtual filesystem (VFS). It lives in SRAM and survives reboots only if you explicitly run save -- otherwise it is wiped on power-off. It is useful for holding scripts and configuration during a session, or for writing and running quick programs on the device.
| Command | What it does |
|---|---|
ls [path] |
List directory contents |
cat <file> |
Print file contents |
touch <file> |
Create an empty file |
mkdir <dir> |
Create a directory |
rm [-r] <path> |
Remove a file or directory tree |
write <file> <text> |
Overwrite a file with one line of text |
write -i <file> |
Interactive multi-line write shell (end with .) |
iwrite <file> |
Same as write -i -- interactive multi-line write |
append <file> <text> |
Append a line of text to a file |
hexdump <file> |
Hex + ASCII dump of a file |
cd [dir] |
Change working directory |
pwd |
Print working directory |
cp <src> <dst> |
Copy a file |
mv <src> <dst> |
Move or rename a file |
stat <path> |
Show file or directory metadata |
edit <file> |
Open a file in the built-in text editor; creates the file if it doesn't exist (see below) |
wc <file> |
Count lines, words, and bytes |
grep <pattern> <file> |
Search a file for a pattern |
find [name] |
Recursive name search |
df |
Filesystem usage summary |
tree |
Print the full directory tree |
save |
Persist the current VFS to flash so it survives reboots |
The editor is a loadable module. It needs three ~32 KB line buffers (~96 KB total), so it does not consume any RAM until you load it. Run
module load editorfirst;editreports an error otherwise. Runmodule unload editorto give the ~96 KB back. See Modules.
> module load editor
> edit /home/blink.ds
> module unload editor # reclaim ~96 KB when you're doneedit <file> opens a small nano-style full-screen editor over the serial
terminal. It auto-detects your terminal size at startup (and falls back to 80×24
if the terminal doesn't report it), so the cursor stays aligned on terminals of
any width. Line-wrapping is disabled while editing so long lines truncate at the
screen edge instead of wrapping.
| Key | Action |
|---|---|
Arrows / Home / End / PgUp / PgDn |
Move the cursor |
Ctrl-S |
Save |
Ctrl-Q / Ctrl-X |
Quit (prompts if unsaved) |
Ctrl-K |
Cut the current line |
Ctrl-Y |
Paste the cut line |
Ctrl-U |
Insert a blank line |
Ctrl-F |
Find (Enter on an empty query repeats the last search) |
Ctrl-Z |
Undo the last structural edit |
Ctrl-G |
Show the shortcut help line |
Files are limited to 64 lines of up to 511 characters each.
Some subsystems carry a large RAM footprint that most sessions never touch. The text editor, for example, needs three line buffers of ~32 KB each (~96 KB) -- more than half of all static RAM -- yet it sits idle unless you actually edit a file.
DeckOS packages such subsystems as modules. A module's buffers are
malloc()-ed from the heap only when you explicitly load it, and freed when you
unload it. A module that isn't loaded costs essentially nothing (just a small
registry entry), and its commands refuse to run until it is loaded.
| Command | What it does |
|---|---|
module / module list |
List modules, their load state, and RAM cost |
module load <name> |
Allocate the module's buffers and enable it |
module unload <name> |
Free the module's buffers |
> module list
modules:
name state ram description
editor - 96 KB nano-style text editor (edit command)
----
loaded RAM: 0 KB
> module load editor # ~96 KB allocated from heap
> edit /home/blink.ds
> module unload editor # ~96 KB returned to heapWhy it matters: with the editor unloaded, idle free heap is ~162 KB; loading
it brings that down to ~66 KB only while you need it. This keeps the DeckScript
interpreter (which malloc()s a 16 KB line buffer per nesting level) from
running out of memory during normal use.
Currently the editor is the one bundled module; the framework is built to host more RAM-heavy subsystems the same way.
DeckOS is a self-contained OS that boots the instant you apply power -- no host required. It cannot boot a PC the way a Linux Live USB does (the RP2040 is an ARM Cortex-M0+ and can't be bootable media for an x86 host), but the chip's USB 1.1 controller lets DeckOS present itself as a composite USB device so it behaves like a portable computer you plug into anything.
When you connect the board it now enumerates as three things at once:
| Interface | What the host sees | DeckOS side |
|---|---|---|
| CDC | A serial port (the shell) | unchanged -- make monitor, minicom, PuTTY |
| MSC | A removable USB drive | a 16 KB FAT12 disk you can drag files to/from |
| HID | A USB keyboard | the board can "type" into the host |
All three are always available -- no rebuilding or mode switching. The serial shell keeps working exactly as before; the mass-storage drive and keyboard are extra.
The device shows up with VID
0x2E8A/ PID0x000B, product name "DeckOS Portable", and a serial number taken from the board's unique ID.
Plug the board into any computer and a small removable drive labelled
DECKOS appears. It's a genuine FAT12 volume (validated with fsck.fat), so
Windows, macOS, and Linux mount it with no drivers. Drop scripts or text files
onto it, then pull them into the DeckOS VFS -- or push VFS files out to the drive
to carry them to another machine.
The disk lives in SRAM (16 KB, 28 usable 512-byte clusters). It is intentionally
small so it doesn't starve the heap the DeckScript interpreter needs -- and the
whole VFS only holds ~16 KB anyway, so a bigger disk could never be filled. It
is wiped on power loss unless you re-create its contents; use the usb command
to bridge it to the persistent VFS.
| Command | What it does |
|---|---|
usb / usb status |
Show mount state, capacity, and file count |
usb list |
List files currently on the USB drive |
usb export <vfspath> [name] |
Copy a VFS file onto the USB drive |
usb import <NAME> [vfsdir] |
Copy a drive file into the VFS (default /home) |
usb sync |
Export every file in /home to the drive |
usb rm <NAME> |
Delete a file from the drive |
usb format |
Wipe and re-create the drive (adds README.TXT) |
> usb export /home/blink.ds # now visible as BLINK.DS on the host drive
> usb list
> usb import SETUP.DS # a file you dragged on from your PC -> /home/SETUP.DS
> usb sync # push all of /home onto the driveNames are converted to 8.3 (blink.ds → BLINK.DS). Files dropped from the
host show up under their short name. To keep imported files across reboots, run
save after importing.
The host owns the filesystem while mounted. DeckOS only touches the disk in short interrupt-safe bursts, so reads/writes from both sides stay consistent.
The board can act as a USB keyboard and type into the connected host -- handy for automation, kiosk setup, or sending a fixed string on demand.
| Command | What it does |
|---|---|
hid / hid status |
Show connection and ready state |
hid type <text...> |
Type the text into the host |
hid line <text...> |
Type the text, then press Enter |
hid key <COMBO...> |
Send one or more key combinations |
hid enter |
Press Enter |
hid key understands modifiers (CTRL, ALT, SHIFT, GUI/WIN/CMD) and
named keys (ENTER, TAB, ESC, SPACE, BACKSPACE, DEL, UP, DOWN,
LEFT, RIGHT, HOME, END, F1–F12, single letters/digits). Join a combo
with +.
> hid type hello from DeckOS
> hid line echo "typed by a microcontroller"
> hid key WIN+r # open the Windows Run dialog
> hid key CTRL+ALT+DEL
> hid key F5 # refreshA device that can inject keystrokes is powerful -- only plug it into machines you trust, and remember it will start typing the moment a command runs.
With an SSD1306 OLED on GP4/GP5 and a battery on VSYS, DeckOS becomes a truly standalone pocket computer: shell output is mirrored to the 128×64 display as a scrolling text console, so you don't need a host terminal to see what's happening. (Input still comes from serial or the Bluetooth shell -- the RP2040's single USB port can't host a keyboard while also being a device.)
| Command | What it does |
|---|---|
console / console status |
Show whether the OLED mirror is on |
console oled on |
Mirror all shell output to the OLED (auto-inits the panel) |
console oled off |
Stop mirroring |
> console oled on
> sysinfo # output now also appears on the OLED
> console oled offThe console shows the last 8 lines (21 characters wide), swallows ANSI escape
codes so they don't appear as garbage, and redraws per line to keep I²C traffic
low. Pair it with the power command to monitor the battery.
DeckScript is DeckOS's built-in scripting language. It runs natively on the Pico -- no interpreter on the host, no serial protocol, just a script file in VFS and a run command. It was written specifically for this environment, so it knows about GPIO pins, ADC channels, PWM, and timing right out of the box.
Scripts are plain text files with a .ds extension by convention. Comments start with #. Everything is line-based -- one statement per line.
# Write a script to VFS
> write /home/blink.ds led on
> append /home/blink.ds sleep 500
> append /home/blink.ds led off
> append /home/blink.ds sleep 500
# Or open the built-in editor and type it properly
> edit /home/blink.ds
# Or use the interactive write shell
> iwrite /home/blink.ds
# Run it
> run /home/blink.ds
# Or through the script command
> script run /home/blink.ds
# Quick self-test
> script testVariables are set with let and referenced with $. Variable names are alphanumeric plus underscores, up to 15 characters.
let x = 10
let name = hello
print the value is $x
print hello $name
There are two special variables available inside scripts:
$_i-- inside arepeatblock, holds the current iteration number (0-based).$return-- after acall, holds the value returned by the function (set vialet return = ...orreturn <value>inside the function body).
Basic integer arithmetic in let expressions: +, -, *, /, %. The operator must be surrounded by spaces. Both sides can be a literal number or a $variable reference.
let x = 10
let y = 3
let z = $x + $y
print $z
let r = $x % $y
print remainder is $r
These all work as the right-hand side of a let statement. Variable references inside the arguments are expanded automatically.
| Function | What it does |
|---|---|
upper(text) |
Convert to uppercase |
lower(text) |
Convert to lowercase |
len(text) |
Length of string |
substr(text, start, length) |
Extract a substring |
contains(haystack, needle) |
Returns 1 if needle found, 0 if not |
trim(text) |
Strip leading and trailing whitespace |
replace(text, old, new) |
Replace all occurrences of old with new |
format(fmt, arg1, arg2, ...) |
Printf-style string formatting |
let msg = hello world
let up = upper($msg)
print $up
let l = len($msg)
print length is $l
let piece = substr($msg, 6, 5)
print $piece
let result = format(%d degrees C, 27)
print $result
| Function | What it does |
|---|---|
sqrt(x) |
Square root |
pow(x, y) |
x to the power of y |
abs(x) |
Absolute value |
min(a, b) |
Smaller of two values |
max(a, b) |
Larger of two values |
clamp(x, lo, hi) |
Clamp x to the range [lo, hi] |
map(x, in_lo, in_hi, out_lo, out_hi) |
Map a value from one range to another |
rand(lo, hi) |
Random integer between lo and hi inclusive |
avg(a, b, c, ...) |
Average of up to 5 values |
let r = sqrt(144)
print $r
let val = clamp(150, 0, 100)
print $val
let mapped = map(512, 0, 1023, 0, 100)
print $mapped
let n = rand(1, 6)
print dice: $n
if $x == 10
print x is ten
elif $x > 10
print x is more than ten
else
print x is less than ten
endif
Comparison operators: ==, !=, <, >, <=, >=. These work for both integers and strings (<, >, <=, >= are integer-only). A bare integer expression is truthy if non-zero; a non-empty string with no operator is always truthy.
switch $color
case red
print stop
case green
print go
default:
print unknown color
endswitch
assert $x == 10 or fail: x should be 10
If the condition is false, the script prints the message and stops.
repeat 5
print iteration $_i
endrepeat
$_i is 0-based and counts up to n - 1. repeat is capped at 10,000 iterations.
let i = 0
while $i < 10
print $i
let i = $i + 1
endwhile
While loops are capped at 100,000 iterations as a safety net.
for i from 1 to 10
print $i
endfor
for i from 0 to 20 step 2
print $i
endfor
for i from 10 to 1 step -1
print $i
endfor
arr_new nums 0
arr_push nums 10
arr_push nums 20
arr_push nums 30
for val in nums
print $val
endfor
You can also use a variable that holds the array name:
let myarr = nums
for val in $myarr
print $val
endfor
for i from 1 to 10
if $i == 5
break
endif
print $i
endfor
Arrays are dynamically sized. Elements are zero-indexed.
arr_new scores 0 # create an empty array called scores
arr_push scores 95
arr_push scores 87
arr_push scores 72
let n = arr_len(scores)
print count: $n
let top = arr_get(scores, 0)
print top score: $top
arr_set scores 1 99 # overwrite element at index 1
arr_dump scores # print all elements
arr_pop scores popped # pop last element into variable 'popped'
print popped: $popped
You can also pre-allocate with a size:
arr_new buffer 10 # 10 elements, all "0"
arr_set buffer 3 hello
Functions are defined anywhere in the script and called with call. Arguments arrive as $arg0, $arg1, etc. The return value is written to the special variable $return -- either by assigning let return = <value> inside the function, or by using return <value> to set it and exit immediately.
def greet
print hello $arg0
enddef
call greet world
call greet DeckOS
def add
let return = $arg0 + $arg1
enddef
call add 3 7
print result: $return
Functions can be defined after the code that calls them -- the interpreter scans the whole script for definitions first. Variable arguments are expanded at the call site before being passed in, so call myfunc $x passes the current value of $x, not the literal string $x.
def fact
if $arg0 <= 1
let return = 1
return
endif
let n = $arg0
let p = $arg0 - 1
call fact $p
let return = $n * $return
enddef
call fact 5
print $return
This is where DeckScript gets useful for actual embedded work. All of these go on the right-hand side of a let.
| Expression | What it does |
|---|---|
adc(0) |
Read ADC channel 0–2 (GP26–28), returns raw 12-bit value |
gpio(15) |
Read the current state of GP15 (0 or 1) |
pwm(16, 50) |
Set GP16 to 50% PWM duty cycle, returns 1 on success |
millis |
Milliseconds since boot |
micros |
Microseconds since boot |
Note:
adc()accepts channels 0, 1, and 2 only (GP26, GP27, GP28). Other channel numbers return 0.
GPIO write and other hardware commands use shell commands directly inside the script:
gpio_write 15 1 # set GP15 high
gpio_write 15 0 # set GP15 low
pulse 15 500 # high for 500 µs then low
wait_pin 15 1 3000 # wait up to 3000 ms for GP15 to go high
sleep 100 # wait 100 ms
wait_pin sets $_timeout to 1 if it times out before the pin changes.
# Read ADC and blink LED faster when value is high
let threshold = 2800
repeat 10
let raw = adc(0)
if $raw > $threshold
gpio_write 25 1
sleep 100
gpio_write 25 0
sleep 100
else
gpio_write 25 1
sleep 500
gpio_write 25 0
sleep 500
endif
endrepeat
print hello
print value is $x
println same thing, also prints a newline
let name = input(what is your name? )
print nice to meet you $name
echo # print a blank line
pause # wait for any keypress before continuing
vars # dump all current variable names and values
log info myapp everything is fine
log warn myapp temperature is high
log err myapp sensor read failed
log debug myapp raw = $raw
assert $x > 0 or fail: x must be positive
Log entries go into the system log and can be read with syslog show.
Scripts can include other scripts with include. The included file runs in the same variable context, so any variables or functions it defines are available after the include.
include /home/helpers.ds
You can also call run from within a script to execute another script file. Unlike include, run creates a fresh variable context each time.
run /home/tests/01_vars.ds
# blink.ds - blink onboard LED 10 times
repeat 10
gpio_write 25 1
sleep 200
gpio_write 25 0
sleep 200
endrepeat
print done
# adc_check.ds
let raw = adc(0)
let mv = map($raw, 0, 4095, 0, 3300)
print raw: $raw
print voltage: $mv mV
if $mv > 2000
print HIGH
elif $mv > 1000
print MID
else
print LOW
endif
# sweep.ds
let pin = 16
let step = 5
for angle from 0 to 180 step $step
servo $pin $angle
sleep 30
endfor
for angle from 180 to 0 step -$step
servo $pin $angle
sleep 30
endfor
print sweep complete
# lib.ds - reusable helpers
def wait_high
# wait for arg0 pin to go high, arg1 timeout ms
wait_pin $arg0 1 $arg1
if $_timeout == 1
print timeout waiting for pin $arg0
endif
enddef
def blink_n
# blink pin arg0 n=arg1 times with arg2 ms delay
repeat $arg1
gpio_write $arg0 1
sleep $arg2
gpio_write $arg0 0
sleep $arg2
endrepeat
enddef
The tone, melody, and piano commands use the RP2040's hardware PWM to drive a passive buzzer. It works well and sounds perfectly fine for a microcontroller.
One important thing: you need a passive buzzer, not an active one. An active buzzer has a built-in oscillator -- it just beeps at one fixed pitch when you apply power and ignores the PWM frequency entirely. A passive buzzer is just a bare piezo element with no internal circuitry, and that's what responds to DeckOS's PWM signal.
Not sure which you have? Apply 3.3V DC to it. If it beeps on its own, it's active. If it stays silent, it's passive -- that's the right one.
Wiring:
GPIO pin ──── [100Ω resistor] ──── (+) Buzzer (−) ──── GND
Any GPIO pin works. GP16 is a good default. The 100 Ω resistor is optional but it protects the pin.
Supported notes use standard scientific pitch notation -- C3 up through B5, sharps and flats included (Bb4 is the same as A#4). The piano command covers C0 through C7. Use REST for silence.
> melody 16 C4:200 D4:200 E4:200 F4:200 G4:200 A4:200 B4:200 C5:400
> tone 16 440 1000
> melody 16 eliseSettings live in the last 4 KB of flash (0x101FF000 on a 2 MB board; the offset scales with the detected flash size on larger parts), protected by a CRC32 checksum. On a fresh board, the checksum won't match so DeckOS falls back to defaults quietly. Nothing breaks, it just uses sensible starting values.
Changes from config set are only in RAM until you run config save. CPU speed and LED state changes take effect on the next boot.
DeckOS keeps a 64-entry ring log in memory. When it fills up, the oldest entries get overwritten. It is wiped on reboot unless a fault handler writes to it first.
> syslog show # everything
> syslog show 20 # last 20 entries
> syslog warn # WARN and above only
> syslog err # errors only
> syslog write myapp "something happened"
> syslog stats
> syslog clearLevels: DBG (grey) → INF (white) → WRN (yellow) → ERR (red).
Background tasks run on Core 1, completely separate from your shell. Each task has a name, a function, and a repeat interval in milliseconds. The scheduler checks for due tasks on every Core 1 tick and fires them when their time is up. Core 1 also drives background jobs such as servo sweeps, the la trigger capture, and other non-blocking work.
> tasks # see all tasks
> tasks disable 0 # stop a task
> tasks enable 0 # bring it back
> top # watch live CPU usage per task
> jobs # list active background Core 1 jobs| Mode | How to trigger | What happens |
|---|---|---|
| Normal | Just power on | Full shell, all drivers loaded; USB enumerates as a composite CDC + MSC + HID device (see USB portable OS) |
| DFU | Run dfu in the shell |
Jumps into USB bootloader so you can reflash |
For hardware recovery (e.g. after flashing a bad build), hold the RP2040's
BOOTSEL button while plugging in USB -- the board mounts as the ROM
mass-storage bootloader (a separate volume from the DeckOS DECKOS drive) so
you can drag a fresh .uf2 onto it. The 1200-baud reset touch also still
reboots the board into BOOTSEL. (Earlier versions used a GP15 recovery pin; that
was removed because it tied up a GPIO and could falsely trigger recovery mode on
boards where the pin floats.)
Drivers are registered and initialised in order at boot. Each one reports OK or FAIL.
| Driver | What it covers |
|---|---|
adc |
ADC channels + internal temperature sensor |
gpio |
GPIO (placeholder, the SDK handles most of this) |
pwm |
PWM (placeholder) |
i2c0 |
I²C bus 0 at 100 kHz on GP4/GP5 |
> driversDeckOS_ESP32 is a port of DeckOS from the RP2040 (bare metal) to the ESP32 (ESP-IDF / FreeRTOS). It introduces a Hardware Abstraction Layer (HAL) that decouples the kernel, shell, scripting, and command logic from the MCU hardware, allowing the same codebase to target both platforms.
| Aspect | RP2040 (this repo) | ESP32 Port |
|---|---|---|
| SDK | Pico SDK (bare metal) | ESP-IDF 5.1 (FreeRTOS) |
| CPU | Dual-core Cortex-M0+ @ 125 MHz | Dual-core Xtensa LX6 @ 240 MHz |
| WiFi | External ESP8266 bridge | Native ESP-IDF WiFi |
| ESP-NOW | Via ESP8266 bridge | Native ESP-NOW |
| Bluetooth | External HC-05 (UART) | Stubbed (BT disabled for DRAM) |
| USB | TinyUSB (CDC + MSC + HID) | ESP-IDF USB CDC only |
| Filesystem | VFS persisted to raw flash | VFS persisted to SPIFFS (2 MB partition) |
| Config | Last 4 KB of flash | NVS (Non-Volatile Storage) |
| Camera | External ESP32-CAM bridge | Native esp32-camera driver |
- Native WiFi — station + AP mode, HTTP server, event-driven
- Native ESP-NOW — peer-to-peer mesh without a router
- ESP32-CAM support — full camera init/capture
- NRF24L01 radio driver — 2.4 GHz SPI radio
- SPIFFS persistence — 2 MB partition for VFS files
- NVS config — key-value storage via ESP-IDF NVS
- PSRAM support — auto-detected and used for heap
- Board auto-detection — ESP32-WROOM vs CAM vs S3
git clone https://github.com/enginestein/DeckOS_ESP32
cd DeckOS_ESP32
idf.py build
idf.py flash monitorRequires ESP-IDF 5.1 or newer and the appropriate xtensa toolchain.
recording.mp4
Who doesn't love a decent shell?