{"id":50445657,"url":"https://github.com/enginestein/deckos","last_synced_at":"2026-05-31T21:03:47.415Z","repository":{"id":356671305,"uuid":"1233514440","full_name":"enginestein/DeckOS","owner":"enginestein","description":"A bare-metal interactive shell OS for the RP2040, with a built-in scripting language, hardware drivers, and wireless support.","archived":false,"fork":false,"pushed_at":"2026-05-29T08:38:15.000Z","size":5247,"stargazers_count":2,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-29T10:20:03.532Z","etag":null,"topics":["bare-metal","bluetooth","electronics","embedded","embedded-systems","firmware","gpio","hobby-os","i2c","microcontroller","operating-system","pico-sdk","raspberry-pi-pico","raspberrypi","rp2040","shell","spi","uart","vfs"],"latest_commit_sha":null,"homepage":"","language":"C","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/enginestein.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-09T03:31:51.000Z","updated_at":"2026-05-29T08:38:18.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/enginestein/DeckOS","commit_stats":null,"previous_names":["enginestein/deckos"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/enginestein/DeckOS","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginestein%2FDeckOS","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginestein%2FDeckOS/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginestein%2FDeckOS/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginestein%2FDeckOS/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/enginestein","download_url":"https://codeload.github.com/enginestein/DeckOS/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/enginestein%2FDeckOS/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33748609,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-31T02:00:06.040Z","response_time":95,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bare-metal","bluetooth","electronics","embedded","embedded-systems","firmware","gpio","hobby-os","i2c","microcontroller","operating-system","pico-sdk","raspberry-pi-pico","raspberrypi","rp2040","shell","spi","uart","vfs"],"created_at":"2026-05-31T21:03:46.461Z","updated_at":"2026-05-31T21:03:47.404Z","avatar_url":"https://github.com/enginestein.png","language":"C","funding_links":[],"categories":[],"sub_categories":[],"readme":"# DeckOS\n\nA bare metal shell OS, currently compatible for RP2040.\n\n\n---\n\n## Table of Contents\n\n- [What you need](#what-you-need)\n- [How it's structured](#how-its-structured)\n- [Getting started](#getting-started)\n- [Using the shell](#using-the-shell)\n- [Commands](#commands)\n  - [Core / Info](#core--info)\n  - [Hardware](#hardware)\n  - [Probes \u0026 Analysis](#probes--analysis)\n  - [OLED / SSD1306](#oled--ssd1306)\n  - [Servo](#servo)\n  - [Audio \u0026 Signalling](#audio--signalling)\n  - [Scripting \u0026 Automation](#scripting--automation)\n  - [System](#system)\n  - [Subsystems](#subsystems)\n  - [WiFi / ESP8266](#wifi--esp8266)\n  - [MQTT](#mqtt)\n  - [Swarm / ESP-NOW](#swarm--esp-now)\n  - [Bluetooth / HC-05](#bluetooth--hc-05)\n  - [Filesystem](#filesystem)\n- [Modules (on-demand RAM)](#modules-on-demand-ram)\n- [USB portable OS](#usb-portable-os)\n  - [USB mass storage (portable disk)](#usb-mass-storage-portable-disk)\n  - [USB keyboard (HID)](#usb-keyboard-hid)\n  - [Standalone handheld (OLED console)](#standalone-handheld-oled-console)\n- [DeckScript](#deckscript)\n  - [Running scripts](#running-scripts)\n  - [Variables](#variables)\n  - [Arithmetic](#arithmetic)\n  - [String functions](#string-functions)\n  - [Math functions](#math-functions)\n  - [Control flow](#control-flow)\n  - [Loops](#loops)\n  - [Arrays](#arrays)\n  - [Functions](#functions)\n  - [Hardware access](#hardware-access-from-scripts)\n  - [I/O](#io)\n  - [Logging and assertions](#logging-and-assertions)\n  - [Includes](#includes)\n  - [Example scripts](#example-scripts)\n- [Buzzer setup](#buzzer-setup)\n- [Config system](#config-system)\n- [Syslog](#syslog)\n- [Scheduler](#scheduler)\n- [Boot modes](#boot-modes)\n- [Drivers](#drivers)\n- [Project layout](#project-layout)\n\n---\n\n## What you need\n\n| Thing | Detail |\n|---|---|\n| Board | Raspberry Pi Pico (RP2040) |\n| Connection | USB to your computer |\n| Optional | Passive buzzer on any GPIO pin (for `tone` / `melody` / `piano`) |\n| Optional | I²C device on GP4 (SDA) and GP5 (SCL) |\n| Optional | SPI device; default pins GP2 (SCK), GP3 (MOSI), GP4 (MISO) |\n| Optional | Servo on any GPIO pin |\n| Optional | ESP8266 module on UART1 (GP5 TX, GP4 RX) for WiFi |\n| Optional | HC-05 Bluetooth module on UART0 for wireless shell |\n\n---\n\n## How it's structured\n\n```\n┌─────────────────────────────────────────────┐\n│                  Core 0                     │\n│   kernel_init() → shell_run() (main loop)   │\n│   commands, I/O, user interaction           │\n└────────────────────┬────────────────────────┘\n                     │  multicore_launch_core1()\n┌────────────────────▼────────────────────────┐\n│                  Core 1                     │\n│   scheduler → background tasks \u0026 jobs       │\n│   (servo sweeps, la trigger capture, ...)   │\n└─────────────────────────────────────────────┘\n\nBoot order:\n  kernel_init()\n    ├── syslog_init()\n    ├── bootloader_run()   (mode detect, config load, banner)\n    ├── drivers_init_all() (adc, gpio, pwm, i2c0)\n    ├── sched_init()       (launches Core 1)\n    └── shell_init()       (registers commands, prints prompt)\n\n  kernel_run()  [infinite loop]\n    └── shell_run()        (non-blocking input → parse → dispatch)\n```\n\n---\n\n## Getting started\n\n### Prerequisites\n\n- [Raspberry Pi Pico SDK](https://github.com/raspberrypi/pico-sdk) v3.0 or newer\n- CMake 3.13+\n- ARM GCC toolchain (`arm-none-eabi-gcc`)\n\n### Build and flash\n\n```bash\ngit clone https://github.com/enginestein/DeckOS\ncd DeckOS\nmake\nmake flash\n```\n\n### Connect\n\n```bash\n# Linux / macOS\nminicom -b 115200 -D /dev/ttyACM0\n\n# Windows - PuTTY or Tera Term, COMx, 115200 8N1\n\n# Or simply:\nmake monitor\n```\n\nYou'll see the boot banner and a `\u003e` prompt. You're in.\n\n---\n\n## Using the shell\n\n```\n\u003e help          ← list everything\n\u003e sysinfo       ← board summary\n\u003e temp          ← core temperature\n\u003e led blink 3   ← blink the LED 3 times\n\u003e gpio read 15  ← read GP15\n```\n\n### Keyboard shortcuts\n\n| Key | What it does |\n|---|---|\n| `↑` / `↓` | Browse command history (last 16 commands; see also the `history` command) |\n| `Backspace` | Delete a character |\n| `Ctrl-C` | Cancel what you're typing |\n| `Ctrl-D` | Quick shortcut for `uptime` |\n| `Ctrl-L` | Clear the screen |\n\n---\n\n## Commands\n\n### Core / Info\n\n| Command | Usage | What it does |\n|---|---|---|\n| `help` | `help` | List all command groups; `help \u003cgroup\u003e` to list a specific group |\n| `version` | `version` | Show OS version and build date |\n| `clear` | `clear` | Clear the screen |\n| `echo` | `echo \u003ctext\u003e` | Print text back |\n| `uptime` | `uptime` | Time since last boot |\n| `sysinfo` | `sysinfo` | Board, CPU, RAM, temp, uptime -- everything at once |\n| `stats` | `stats` | Command counts, CPU speed, temperature |\n| `top` | `top` | Live task monitor showing Core 1 CPU usage per task; press any key to exit |\n| `jobs` | `jobs` | List all active background Core 1 jobs |\n| `jobs` | `jobs cancel \u003cid\u003e` | Cancel a specific background job |\n\n### Hardware\n\n| Command | Usage | What it does |\n|---|---|---|\n| `temp` | `temp` | Internal chip temperature in °C and °F |\n| `mem` | `mem` | Available heap and flash sizes |\n| `memmap` | `memmap` | Full memory map with SRAM sections, stack, heap, and peripheral addresses |\n| `free` | `free` | Heap allocator stats and a list of every live allocation |\n| `stack` | `stack` | Core 0 stack depth, current usage percentage, free headroom, and canary high-water mark |\n| `power` | `power` | VSYS voltage via ADC3 (GP29), source detection, and LiPo / AA battery percentage estimate |\n| `led` | `led \u003con\\|off\\|toggle\\|blink [n]\u003e` | Control the onboard LED (GP25). `blink n` blinks it n times |\n| `gpio` | `gpio \u003cread\\|write\\|mode\\|irq\u003e \u003cpin\u003e [val]` | Read, write, set direction, or monitor a GPIO pin |\n| `pwm` | `pwm \u003cpin\u003e \u003cduty 0–100\u003e [freq_hz]` | Set PWM duty cycle on any pin; optional frequency argument |\n| `adc` | `adc \u003c0\\|1\\|2\u003e` | Read ADC channel 0–2 (GP26–28), shows raw value and voltage |\n| `avg` | `avg \u003cch\u003e [samples]` | Same as `adc` but averages multiple samples; shows min, max, and peak-to-peak noise |\n| `pull` | `pull \u003cpin\u003e \u003cup\\|down\\|none\u003e` | Set a pin's pull resistor |\n| `clock` | `clock [mhz]` | Get or set CPU speed (48–200 MHz) |\n| `pin` | `pin` | Snapshot of all GPIO pin states (direction and value) |\n| `pinout` | `pinout` | ASCII Pico pinout diagram with live pin states, directions, and functions |\n| `uid` | `uid` | Print the board's unique 64-bit ID |\n| `wdog` | `wdog` | Check if the last reboot was caused by the watchdog |\n| `i2c` | `i2c scan [sda scl] \\| read \u003caddr\u003e \u003creg\u003e \\| write \u003caddr\u003e \u003creg\u003e \u003cval\u003e \\| dump \u003caddr\u003e` | I²C bus tools (default GP4=SDA, GP5=SCL) |\n| `spi` | `spi init \\| write \\| read \\| xfer` | SPI bus operations (see below) |\n| `uart` | `uart \u003cbaud\u003e \u003ctx_pin\u003e \u003crx_pin\u003e [timeout_s]` | Bridge USB-CDC to a hardware UART; press Ctrl-X to exit |\n| `flash` | `flash read \\| write \\| erase \u003caddr\u003e` | Raw flash read/write/erase (see below) |\n\n#### Stack inspection\n\n`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.\n\n```bash\n\u003e stack\nstack usage (Core 0):\n  top        : 0x20041C00\n  limit      : 0x20040000\n  size       : 7168 B  (7 KB)\n  SP now     : 0x20041800\n  used       : 1024 B  (14.3%)\n  free       : 6144 B\n  peak used  : unknown (stack not pre-filled with canary)\n```\n\n#### Power / VSYS\n\n`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.\n\n```bash\n\u003e power\n```\n\n\u003e GP29 must not be driven externally when using this command.\n\n#### Live GPIO monitoring\n\n```bash\n\u003e gpio irq 15           # watch GP15 for edges (30 s default)\n\u003e gpio irq 15 60        # watch for up to 60 s\n\u003e gpio irq 15 dump      # print everything captured so far\n\u003e gpio irq 15 stop      # stop watching\n```\n\n#### I²C bus\n\nDefault pins are GP4 (SDA) and GP5 (SCL). All subcommands accept optional `[sda] [scl]` pin overrides.\n\n```bash\n\u003e i2c scan                      # scan on default pins\n\u003e i2c scan 6 7                  # scan on GP6/GP7\n\u003e i2c read  0x68 0x75           # read one byte\n\u003e i2c write 0x68 0x6B 0x00      # write one byte\n\u003e i2c dump  0x68                # dump all 256 registers\n```\n\n#### SPI bus\n\nSPI must be initialised before use. Default pins are GP2 (SCK), GP3 (MOSI), GP4 (MISO) at 1 MHz.\n\n```bash\n\u003e spi init                          # default pins and baud\n\u003e spi init 2 3 4 500000             # custom: sck mosi miso baud\n\u003e spi write \u003ccs_pin\u003e \u003chex bytes\u003e    # write bytes to a device\n\u003e spi read  \u003ccs_pin\u003e \u003creg_hex\u003e [n]  # read n bytes from a register\n\u003e spi xfer  \u003ccs_pin\u003e \u003chex bytes\u003e    # full-duplex transfer, prints RX\n```\n\n#### UART passthrough\n\nBridges the USB serial port to a hardware UART. Useful for talking to other devices from the DeckOS shell.\n\n```bash\n\u003e uart 9600 0 1          # 9600 baud, TX=GP0, RX=GP1\n\u003e uart 115200 4 5 30     # 115200 baud, TX=GP4, RX=GP5, 30s timeout\n```\n\nUART0 TX pins: GP0, GP12, GP16. UART1 TX pins: GP4, GP8. Press Ctrl-X to exit.\n\n#### Flash access\n\nAddresses 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.\n\n```bash\n\u003e flash read  10000 64      # dump 64 bytes starting at offset 0x10000\n\u003e flash erase 10000         # erase the 4 KB sector containing 0x10000\n\u003e flash write 10000 DE AD   # write bytes (address must be 256-byte page-aligned)\n```\n\n### Probes \u0026 Analysis\n\n| Command | Usage | What it does |\n|---|---|---|\n| `la` | `la \u003cpin\u003e [samples] [us_per_sample] [trigger]` | Logic analyser -- sample a pin and render an ASCII timing diagram |\n| `scope` | `scope \u003cpin\u003e \u003cexpected_hz\u003e \u003cduration_ms\u003e` | Clean waveform viewer; auto-selects sample rate from expected frequency |\n| `detect` | `detect [sda scl] \\| uart \u003cpin\u003e [timeout_s] \\| analyze \u003cpin\u003e [samples] [us]` | Scan I²C bus, probe for UART baud rate, or guess a protocol from logic samples |\n| `imu` | `imu \u003cread\\|stream\\|attitude\\|calibrate\\|raw\\|whoami\u003e` | MPU6050 accelerometer/gyro on I²C (GP4/GP5) |\n\n#### Logic analyser\n\n```bash\n\u003e la 15                    # 128 samples at 10 µs each\n\u003e la 15 256 5              # 256 samples at 5 µs each\n\u003e la 15 256 2 trigger      # background: wait for falling edge then capture\n\u003e jobs                     # check trigger status\n\u003e jobs cancel 0            # abort if needed\n```\n\nAfter 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.\n\n#### Scope\n\n`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.\n\n```bash\n\u003e scope 15 1000 50     # view a ~1 kHz signal for 50 ms\n\u003e scope 15 50 500      # view a ~50 Hz signal for 500 ms\n```\n\n#### Device detection\n\n```bash\n\u003e detect                     # scan I²C and report known devices\n\u003e detect 6 7                 # scan on custom SDA/SCL pins\n\u003e detect uart 1              # probe GP1 for UART activity\n\u003e detect uart 1 5            # same, 5 second timeout\n\u003e detect analyze 15          # sample GP15 and guess the protocol\n\u003e detect analyze 15 256 5    # 256 samples at 5 µs each\n```\n\n### OLED / SSD1306\n\n| Command | Usage | What it does |\n|---|---|---|\n| `oled init` | `oled init` | Initialise display on GP4 (SDA) / GP5 (SCL) |\n| `oled on / off` | `oled on` | Power display on or off |\n| `oled clear` | `oled clear` | Blank framebuffer and flush |\n| `oled fill` | `oled fill \u003chex\u003e` | Fill framebuffer with a pattern byte (e.g. `AA`) and flush |\n| `oled flush` | `oled flush` | Push framebuffer to screen |\n| `oled contrast` | `oled contrast \u003c0–255\u003e` | Set brightness |\n| `oled invert` | `oled invert \u003c0\\|1\u003e` | Invert display colours |\n| `oled flip` | `oled flip \u003ch:0\\|1\u003e \u003cv:0\\|1\u003e` | Mirror display horizontally and/or vertically |\n| `oled text` | `oled text \u003ccol\u003e \u003crow\u003e \u003cstr\u003e` | Draw text at a character-grid cell |\n| `oled textxy` | `oled textxy \u003cx\u003e \u003cy\u003e \u003cstr\u003e` | Draw text at pixel coordinates |\n| `oled printf` | `oled printf \u003ccol\u003e \u003crow\u003e \u003cfmt...\u003e` | Formatted text at a grid cell |\n| `oled pixel` | `oled pixel \u003cx\u003e \u003cy\u003e \u003c0\\|1\u003e` | Set or clear a single pixel |\n| `oled line` | `oled line \u003cx0\u003e \u003cy0\u003e \u003cx1\u003e \u003cy1\u003e` | Draw a line |\n| `oled hline` | `oled hline \u003cx0\u003e \u003cx1\u003e \u003cy\u003e` | Horizontal line |\n| `oled vline` | `oled vline \u003cx\u003e \u003cy0\u003e \u003cy1\u003e` | Vertical line |\n| `oled rect` | `oled rect \u003cx\u003e \u003cy\u003e \u003cw\u003e \u003ch\u003e` | Rectangle outline |\n| `oled rectfill` | `oled rectfill \u003cx\u003e \u003cy\u003e \u003cw\u003e \u003ch\u003e` | Filled rectangle |\n| `oled circle` | `oled circle \u003ccx\u003e \u003ccy\u003e \u003cr\u003e` | Circle outline |\n| `oled circlefill` | `oled circlefill \u003ccx\u003e \u003ccy\u003e \u003cr\u003e` | Filled circle |\n| `oled progress` | `oled progress \u003cx\u003e \u003cy\u003e \u003cw\u003e \u003ch\u003e \u003c%\u003e` | Progress bar |\n| `oled title` | `oled title \u003ctext\u003e` | Title bar at row 0 (inverted) |\n| `oled status` | `oled status \u003cleft\u003e \u003cright\u003e` | Status bar on bottom row |\n| `oled splash` | `oled splash \u003cline1\u003e \u003cline2\u003e` | Splash screen and flush |\n| `oled notify` | `oled notify \u003cmsg\u003e \u003cms\u003e` | Timed notification overlay (100–30000 ms) |\n| `oled scroll` | `oled scroll \u003cright\\|left\u003e \u003csp\u003e \u003cep\u003e` | Hardware scroll between pages sp–ep |\n| `oled scroll stop` | `oled scroll stop` | Stop hardware scroll |\n| `oled spinner` | `oled spinner \u003cx\u003e \u003cy\u003e \u003cframe 0–7\u003e` | Spinner glyph at pixel position |\n| `oled boot` | `oled boot` | Animated boot sequence |\n\n### Servo\n\n| Command | Usage | What it does |\n|---|---|---|\n| `servo` | `servo \u003cpin\u003e \u003cangle 0–180\u003e` | Move a servo to an absolute angle |\n| `servo sweep` | `servo sweep \u003cpin\u003e [from to step_ms]` | Blocking sweep from one angle to another |\n| `servo bg` | `servo bg \u003cpin\u003e sweep [min max step step_ms]` | Background sweep (Core 1), non-blocking |\n| `servo bg` | `servo bg \u003cpin\u003e goto \u003cangle\u003e [step_ms]` | Move to angle in background |\n| `servo bg` | `servo bg \u003cpin\u003e stop` | Stop a background servo |\n| `servo bg list` | `servo bg list` | List all active background servos |\n\n```bash\n\u003e servo 16 90                       # centre a servo on GP16\n\u003e servo sweep 16 0 180 20           # sweep 0→180° with 20 ms steps\n\u003e servo bg 16 sweep 0 180 1 15      # background sweep, 1° steps every 15 ms\n\u003e servo bg 16 goto 45               # move to 45° in background\n\u003e servo bg 16 stop\n```\n\n### Audio \u0026 Signalling\n\n| Command | Usage | What it does |\n|---|---|---|\n| `tone` | `tone \u003cpin\u003e \u003cnote\\|hz\u003e [ms]` | Play a tone on a buzzer -- use a note name like `C4` or `A#3`, or a raw Hz value |\n| `melody` | `melody \u003cpin\u003e \u003cC4:200 E4:200 ...\u003e` | Play a sequence of notes. Format is `NOTE:duration_ms`; use `REST` for silence |\n| `melody` | `melody \u003cpin\u003e elise` | Play the full Für Elise arrangement |\n| `melody` | `melody \u003cpin\u003e canon` | Play the full Canon in D arrangement |\n| `morse` | `morse \u003ctext\u003e [wpm]` | Blink the onboard LED in morse code (default 13 WPM) |\n| `piano` | `piano \u003cpin\u003e [duration_ms]` | Interactive keyboard piano -- drive a buzzer from keypresses |\n\n```bash\n\u003e tone 16 A4 500\n\u003e tone 16 440 500\n\u003e melody 16 C4:200 E4:200 G4:400 REST:100 C5:600\n\u003e melody 16 elise\n\u003e melody 16 canon\n\u003e morse SOS\n\u003e morse HELLO 20\n\u003e piano 16\n\u003e piano 16 200          # 200 ms per note\n```\n\n#### Piano keyboard layout\n\n```\nBlack keys:  W  E     T  Y  U     O  P   (sharps/flats)\nWhite keys: A  S  D  F  G  H  J  K  L  ;\n```\n\n`[` and `]` shift the base octave down and up. Press `q` to quit.\n\n### Scripting \u0026 Automation\n\n| Command | Usage | What it does |\n|---|---|---|\n| `sleep` | `sleep \u003cms\u003e` | Wait for a number of milliseconds (1–30000) |\n| `repeat` | `repeat \u003cn\u003e \u003ccommand\u003e` | Run a command n times in a row (1–100) |\n| `watch` | `watch \u003cms\u003e \u003ccommand\u003e` | Run a command repeatedly at an interval; press any key to stop |\n| `trigger` | `trigger \u003cpin\u003e \u003crise\\|fall\\|both\u003e \u003ccommand\u003e` | Watch a pin and run a command the moment an edge fires (one-shot) |\n| `cron` | `cron \u003cdelay_ms\u003e \u003ccommand\u003e` | Schedule a command to run once after a delay; returns immediately |\n| `bench` | `bench \u003citerations\u003e \u003ccommand\u003e` | Run a command many times and report throughput and timing |\n| `time` | `time \u003ccommand\u003e` | Run a command and report how long it took (ms and µs) |\n| `alias` | `alias [name [command...]]` | Define, list, or show command aliases |\n| `unalias` | `unalias \u003cname\u003e` | Remove an alias |\n| `run` | `run \u003cfile\u003e` | Run a DeckScript file from VFS |\n| `script` | `script run \u003cfile\u003e` | Same as `run` |\n| `script` | `script test` | Run the built-in DeckScript self-test |\n| `edit` | `edit \u003cfile\u003e` | Open a file in the built-in nano-style text editor |\n\n```bash\n\u003e watch 1000 temp\n\u003e watch 500 adc 0\n\u003e trigger 15 fall led on\n\u003e repeat 5 led toggle\n\u003e cron 5000 reboot\n\u003e bench 1000 echo hi\n\u003e time sysinfo\n\u003e run /home/blink.ds\n\u003e edit /home/blink.ds\n```\n\n#### Aliases\n\nAliases let you give a short name to a longer command. The alias name is replaced\nby its value, and any extra arguments you type are appended. Expansion happens\nexactly **once**, so a self-referential alias such as `alias ls \"ls -l\"` is safe\nand will not loop.\n\n```bash\n\u003e alias ll ls               # 'll' now runs 'ls'\n\u003e alias t temp              # 't' now runs 'temp'\n\u003e ll /home                  # expands to: ls /home\n\u003e alias                     # list all defined aliases\n\u003e alias ll                  # show one alias\n\u003e unalias ll                # remove it\n```\n\nUp to 16 aliases can be defined. Aliases live in RAM and are cleared on reboot.\n\n### System\n\n| Command | Usage | What it does |\n|---|---|---|\n| `reboot` | `reboot` | Save VFS to flash then reboot via watchdog after 1 second |\n| `dfu` | `dfu` | Jump into USB bootloader mode for reflashing |\n| `uid` | `uid` | Print the board's unique 64-bit ID |\n| `wdog` | `wdog` | Check if the last reboot was caused by the watchdog |\n| `fault` | `fault info` | Show CPU registers captured from the last HardFault this session |\n| `fault` | `fault test` | Trigger a deliberate fault to verify the handler and syslog integration |\n| `date` | `date` | Show the current date and time from the RP2040 RTC |\n| `date` | `date set \u003cY\u003e \u003cM\u003e \u003cD\u003e \u003ch\u003e \u003cm\u003e \u003cs\u003e` | Set the real-time clock |\n| `history` | `history` | List the command history (last 16 commands) |\n| `history` | `history clear` | Clear the command history |\n| `uname` | `uname [-a]` | Print the OS name; `-a` shows full system identity |\n| `rand` | `rand [min] [max]` | Generate a hardware random number |\n\nThe 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.\n\n```bash\n\u003e fault info\n\u003e fault test     # board will reboot; check 'syslog err' after\n```\n\n#### Real-time clock (`date`)\n\n`date` uses the RP2040's built-in hardware RTC. The clock starts at\n`2025-01-01 00:00:00` on boot and keeps ticking while the board is powered. Set\nit once per session to get accurate timestamps. The day of the week is computed\nautomatically.\n\n```bash\n\u003e date                              # show current time\n\u003e date set 2026 5 30 19 45 00       # YYYY MM DD hh mm ss\n```\n\n\u003e The RP2040 RTC is **not** battery-backed -- the time resets on every power loss.\n\n#### Random numbers (`rand`)\n\n`rand` draws from the RP2040's hardware ring-oscillator entropy source, so the\nvalues are genuinely random (unlike a seeded software PRNG).\n\n```bash\n\u003e rand                  # full 32-bit value\n\u003e rand 6                # 0 .. 5  (e.g. a die index)\n\u003e rand 1 6              # 1 .. 6  inclusive\n```\n\n#### System identity (`uname`)\n\n```bash\n\u003e uname                 # -\u003e DeckOS\n\u003e uname -a              # DeckOS 5.0 \u003chostname\u003e RP2040 Cortex-M0+ \u003cmhz\u003e SRAM=.. flash=.. \u003cboard\u003e\n```\n\n### Subsystems\n\n| Command | Usage | What it does |\n|---|---|---|\n| `drivers` | `drivers` | Show all drivers and whether they initialised cleanly |\n| `tasks` | `tasks [enable\\|disable \u003cid\u003e]` | View or toggle background scheduler tasks |\n| `config` | `config show\\|set \u003ckey\u003e \u003cval\u003e\\|save\\|reset` | Manage persistent settings |\n| `syslog` | `syslog show [n]\\|warn\\|err\\|write \u003ctag\u003e \u003cmsg\u003e\\|clear\\|stats` | Read and manage the in-memory log |\n\n**Config keys you can set:**\n\n| Key | Values | Effect |\n|---|---|---|\n| `hostname` | any string | Name shown at boot |\n| `cpu_mhz` | 48–200, or 0 for default 125 | CPU speed set at boot |\n| `boot_led` | 0 or 1 | Whether the LED turns on at boot |\n\n```bash\n\u003e config set hostname my-pico\n\u003e config set cpu_mhz 133\n\u003e config set boot_led 1\n\u003e config save\n\u003e config show\n\u003e config reset\n```\n\n---\n\n## WiFi / ESP8266\n\nDeckOS 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.\n\n### What WiFi lets you do\n\n- **Scan nearby networks** -- see every access point in range with signal strength and security type\n- **Connect to WiFi** -- join a network so the ESP8266 has an IP address\n- **HTTP GET and POST** -- pull data from or push data to any URL\n- **Telnet server** -- accept incoming terminal connections on port 23\n- **HTTP server** -- turn the Pico into a tiny web endpoint on port 80\n- **Remote monitoring** -- push sensor readings to a server without a USB cable near the Pico\n- **Wireless data logging** -- log GPIO events, ADC samples, or syslog entries to a remote server\n- **Headless deployment** -- once configured, the Pico can operate without USB and still participate in your network\n\n### Wiring\n\n| ESP8266 pin | Pico pin | Notes |\n|---|---|---|\n| TX | GP5 | |\n| RX | GP4 | |\n| VCC / 3V3 | External 3.3V | Use a dedicated regulator; the Pico's 3V3 pin may brown out under WiFi load |\n| GND | GND | Common ground required |\n| EN / CH_PD | 3.3V | Must be HIGH for the module to boot |\n\n### Bridge firmware setup\n\nFlash `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:\n\n```cpp\nString wifi_ssid     = \"YourNetworkName\";\nString wifi_password = \"YourPassword\";\n```\n\nThe bridge starts in auto-detect mode and responds to `@`-prefixed control commands from the Pico.\n\n### WiFi commands\n\n| Command | What it does |\n|---|---|\n| `wifi init` | Initialise the ESP8266 on UART1 at 115200 baud |\n| `wifi init \u003cbaud\u003e` | Initialise at a custom baud rate |\n| `wifi status` | Show UART config and ready state |\n| `wifi ping` | Query the bridge and show its current status |\n| `wifi scan` | Scan for WiFi networks |\n| `wifi join \u003cssid\u003e \u003cpass\u003e` | Join a network |\n| `wifi ip` | Show the assigned IP address |\n| `wifi shell` | Drop into a raw interactive shell with the ESP8266 |\n| `wifi deinit` | Release UART1 from the ESP8266 |\n| `wifi get \u003curl\u003e` | HTTP GET request |\n| `wifi post \u003curl\u003e \u003cbody\u003e` | HTTP POST request |\n| `wifi serve` | Start an HTTP server on port 80 |\n| `wifi telnet` | Start a telnet server on port 23 |\n| `wifi telnet stop` | Stop the telnet server |\n| `wifi bridge status` | Show bridge mode, WiFi connection state, and IP address |\n| `wifi bridge scan` | Scan for nearby WiFi networks via bridge |\n| `wifi bridge connect` | Connect using credentials stored in the bridge firmware |\n| `wifi bridge reset` | Reboot the ESP8266 |\n| `wifi bridge auto` | Switch bridge to auto-detect firmware mode |\n| `wifi bridge at` | Switch bridge to raw AT command passthrough |\n| `wifi bridge raw` | Switch bridge to raw command mode |\n\n### Typical workflow\n\n```bash\n\u003e wifi init                  # start the UART link to the ESP8266\n\u003e wifi bridge status         # confirm the bridge is alive and check WiFi state\n\u003e wifi bridge scan           # see what networks are in range\n\u003e wifi bridge connect        # connect using credentials in the bridge firmware\n\u003e wifi bridge status         # confirm connection and get the IP address\n\u003e wifi get http://example.com/data\n\u003e wifi post http://example.com/log \"temp=27.3\"\n```\n\n---\n\n## MQTT\n\n| Command | What it does |\n|---|---|\n| `mqtt server \u003chost\u003e` | Set broker address |\n| `mqtt port \u003cn\u003e` | Set broker port (default 1883) |\n| `mqtt id \u003cname\u003e` | Set client ID |\n| `mqtt connect` | Connect to broker |\n| `mqtt disconnect` | Disconnect from broker |\n| `mqtt status` | Show connection state |\n| `mqtt pub \u003ctopic\u003e \u003cmsg\u003e` | Publish a message |\n| `mqtt sub \u003ctopic\u003e` | Subscribe to a topic |\n| `mqtt unsub \u003ctopic\u003e` | Unsubscribe from a topic |\n\nRequires WiFi to be initialised and connected first.\n\n```bash\n\u003e wifi init\n\u003e wifi bridge connect\n\u003e mqtt server 192.168.1.100\n\u003e mqtt connect\n\u003e mqtt pub deck/temp 23.4\n\u003e mqtt sub deck/cmd/#\n```\n\n---\n\n## Swarm / ESP-NOW\n\nLets multiple Picos communicate peer-to-peer without a router, useful for drone swarms or sensor meshes.\n\n| Command | What it does |\n|---|---|\n| `swarm init` | Start ESP-NOW mesh |\n| `swarm id \u003cname\u003e` | Set this node's name |\n| `swarm mac` | Show this node's MAC address |\n| `swarm peer \u003cMAC\u003e` | Register a peer node |\n| `swarm pub \u003clat\u003e \u003clon\u003e \u003calt\u003e \u003chdg\u003e \u003cstate\u003e` | Broadcast position telemetry |\n| `swarm list` | Show all known peers |\n| `swarm stop` | Stop the mesh |\n\n```bash\n# Node 1\n\u003e wifi init\n\u003e swarm init\n\u003e swarm mac          # note MAC: AA:BB:CC:DD:EE:01\n\u003e swarm id drone1\n\n# Node 2\n\u003e wifi init\n\u003e swarm init\n\u003e swarm mac          # note MAC: AA:BB:CC:DD:EE:02\n\u003e swarm id drone2\n\u003e swarm peer AA:BB:CC:DD:EE:01   # register node 1\n\n# Broadcast from node 1\n\u003e swarm pub 28.6139 77.2090 100.0 180.0 1\n```\n\n---\n\n## Bluetooth / HC-05\n\nDeckOS 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.\n\n### What Bluetooth lets you do\n\n- **Wireless shell** -- run any DeckOS command from a Bluetooth terminal on your phone or laptop\n- **Remote command execution** -- send a single command from the BT side and get the output back\n- **Live stats streaming** -- stream `top`-style CPU and temperature data to a connected Bluetooth client\n- **Syslog mirroring** -- forward every log entry to the Bluetooth terminal as it happens\n- **File transfer** -- send and receive VFS files over the Bluetooth link\n\n### Wiring\n\n```\nHC-05 VCC  →  VSYS / 5V / VBUS\nHC-05 GND  →  GND\nHC-05 RXD  →  GP1\nHC-05 TXD  →  GP0\n```\n\n### Bluetooth commands\n\n| Command | What it does |\n|---|---|\n| `bt init [baud]` | Initialise the HC-05 UART |\n| `bt status` | Show init state, connection state, log mirror state |\n| `bt shell` | Start a full wireless DeckOS terminal over Bluetooth |\n| `bt exec \u003ccommand\u003e` | Run one command and send the output to the BT client |\n| `bt top [ms]` | Stream live CPU/temp stats to the BT client |\n| `bt log on` | Mirror all syslog entries to the BT client in real time |\n| `bt log off` | Stop mirroring |\n| `bt send \u003cfile\u003e` | Send a VFS file over Bluetooth |\n| `bt recv \u003cfile\u003e` | Receive a file from Bluetooth into VFS |\n| `bt sniff [s]` | Raw byte sniffer -- prints hex and ASCII |\n| `bt at` | Drop into interactive AT command mode |\n| `bt name \u003cname\u003e` | Set the HC-05 module name (requires AT mode) |\n| `bt pin \u003ccode\u003e` | Set the pairing PIN (requires AT mode) |\n| `bt baud \u003crate\u003e` | Change the HC-05 UART baud rate (requires AT mode) |\n\n### Typical workflow\n\n```bash\n\u003e bt init\n\u003e bt status\n\u003e bt shell       # connect from your phone now\n```\n\n---\n\n## Filesystem\n\nDeckOS 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.\n\n| Command | What it does |\n|---|---|\n| `ls [path]` | List directory contents |\n| `cat \u003cfile\u003e` | Print file contents |\n| `touch \u003cfile\u003e` | Create an empty file |\n| `mkdir \u003cdir\u003e` | Create a directory |\n| `rm [-r] \u003cpath\u003e` | Remove a file or directory tree |\n| `write \u003cfile\u003e \u003ctext\u003e` | Overwrite a file with one line of text |\n| `write -i \u003cfile\u003e` | Interactive multi-line write shell (end with `.`) |\n| `iwrite \u003cfile\u003e` | Same as `write -i` -- interactive multi-line write |\n| `append \u003cfile\u003e \u003ctext\u003e` | Append a line of text to a file |\n| `hexdump \u003cfile\u003e` | Hex + ASCII dump of a file |\n| `cd [dir]` | Change working directory |\n| `pwd` | Print working directory |\n| `cp \u003csrc\u003e \u003cdst\u003e` | Copy a file |\n| `mv \u003csrc\u003e \u003cdst\u003e` | Move or rename a file |\n| `stat \u003cpath\u003e` | Show file or directory metadata |\n| `edit \u003cfile\u003e` | Open a file in the built-in text editor; creates the file if it doesn't exist (see below) |\n| `wc \u003cfile\u003e` | Count lines, words, and bytes |\n| `grep \u003cpattern\u003e \u003cfile\u003e` | Search a file for a pattern |\n| `find [name]` | Recursive name search |\n| `df` | Filesystem usage summary |\n| `tree` | Print the full directory tree |\n| `save` | Persist the current VFS to flash so it survives reboots |\n\n### Text editor (`edit`)\n\n\u003e **The editor is a loadable module.** It needs three ~32 KB line buffers\n\u003e (~96 KB total), so it does **not** consume any RAM until you load it. Run\n\u003e `module load editor` first; `edit` reports an error otherwise. Run\n\u003e `module unload editor` to give the ~96 KB back. See\n\u003e [Modules](#modules-on-demand-ram).\n\n```bash\n\u003e module load editor\n\u003e edit /home/blink.ds\n\u003e module unload editor      # reclaim ~96 KB when you're done\n```\n\n`edit \u003cfile\u003e` opens a small nano-style full-screen editor over the serial\nterminal. It auto-detects your terminal size at startup (and falls back to 80×24\nif the terminal doesn't report it), so the cursor stays aligned on terminals of\nany width. Line-wrapping is disabled while editing so long lines truncate at the\nscreen edge instead of wrapping.\n\n| Key | Action |\n|---|---|\n| Arrows / `Home` / `End` / `PgUp` / `PgDn` | Move the cursor |\n| `Ctrl-S` | Save |\n| `Ctrl-Q` / `Ctrl-X` | Quit (prompts if unsaved) |\n| `Ctrl-K` | Cut the current line |\n| `Ctrl-Y` | Paste the cut line |\n| `Ctrl-U` | Insert a blank line |\n| `Ctrl-F` | Find (Enter on an empty query repeats the last search) |\n| `Ctrl-Z` | Undo the last structural edit |\n| `Ctrl-G` | Show the shortcut help line |\n\nFiles are limited to 64 lines of up to 511 characters each.\n\n---\n\n## Modules (on-demand RAM)\n\nSome subsystems carry a large RAM footprint that most sessions never touch. The\ntext editor, for example, needs three line buffers of ~32 KB each (~96 KB) --\nmore than half of all static RAM -- yet it sits idle unless you actually edit a\nfile.\n\nDeckOS packages such subsystems as **modules**. A module's buffers are\n`malloc()`-ed from the heap only when you explicitly load it, and freed when you\nunload it. A module that isn't loaded costs essentially nothing (just a small\nregistry entry), and its commands refuse to run until it is loaded.\n\n| Command | What it does |\n|---|---|\n| `module` / `module list` | List modules, their load state, and RAM cost |\n| `module load \u003cname\u003e` | Allocate the module's buffers and enable it |\n| `module unload \u003cname\u003e` | Free the module's buffers |\n\n```bash\n\u003e module list\nmodules:\n  name       state  ram      description\n  editor     -        96 KB  nano-style text editor (edit command)\n  ----\n  loaded RAM: 0 KB\n\u003e module load editor       # ~96 KB allocated from heap\n\u003e edit /home/blink.ds\n\u003e module unload editor      # ~96 KB returned to heap\n```\n\n**Why it matters:** with the editor unloaded, idle free heap is ~162 KB; loading\nit brings that down to ~66 KB only while you need it. This keeps the DeckScript\ninterpreter (which `malloc()`s a 16 KB line buffer per nesting level) from\nrunning out of memory during normal use.\n\nCurrently the **editor** is the one bundled module; the framework is built to\nhost more RAM-heavy subsystems the same way.\n\n---\n\n## USB portable OS\n\nDeckOS is a self-contained OS that boots the instant you apply power -- no host\nrequired. It cannot boot a PC the way a Linux Live USB does (the RP2040 is an\nARM Cortex-M0+ and can't be bootable media for an x86 host), but the chip's\nUSB 1.1 controller lets DeckOS present itself as a **composite USB device** so\nit behaves like a portable computer you plug into anything.\n\nWhen you connect the board it now enumerates as three things at once:\n\n| Interface | What the host sees | DeckOS side |\n|---|---|---|\n| **CDC** | A serial port (the shell) | unchanged -- `make monitor`, minicom, PuTTY |\n| **MSC** | A removable USB drive | a 16 KB FAT12 disk you can drag files to/from |\n| **HID** | A USB keyboard | the board can \"type\" into the host |\n\nAll three are always available -- no rebuilding or mode switching. The serial\nshell keeps working exactly as before; the mass-storage drive and keyboard are\nextra.\n\n\u003e The device shows up with VID `0x2E8A` / PID `0x000B`, product name\n\u003e **\"DeckOS Portable\"**, and a serial number taken from the board's unique ID.\n\n### USB mass storage (portable disk)\n\nPlug the board into any computer and a small removable drive labelled\n**DECKOS** appears. It's a genuine FAT12 volume (validated with `fsck.fat`), so\nWindows, macOS, and Linux mount it with no drivers. Drop scripts or text files\nonto it, then pull them into the DeckOS VFS -- or push VFS files out to the drive\nto carry them to another machine.\n\nThe disk lives in SRAM (16 KB, 28 usable 512-byte clusters). It is intentionally\nsmall so it doesn't starve the heap the DeckScript interpreter needs -- and the\nwhole VFS only holds ~16 KB anyway, so a bigger disk could never be filled. It\nis wiped on power loss unless you re-create its contents; use the `usb` command\nto bridge it to the persistent VFS.\n\n| Command | What it does |\n|---|---|\n| `usb` / `usb status` | Show mount state, capacity, and file count |\n| `usb list` | List files currently on the USB drive |\n| `usb export \u003cvfspath\u003e [name]` | Copy a VFS file onto the USB drive |\n| `usb import \u003cNAME\u003e [vfsdir]` | Copy a drive file into the VFS (default `/home`) |\n| `usb sync` | Export every file in `/home` to the drive |\n| `usb rm \u003cNAME\u003e` | Delete a file from the drive |\n| `usb format` | Wipe and re-create the drive (adds `README.TXT`) |\n\n```bash\n\u003e usb export /home/blink.ds        # now visible as BLINK.DS on the host drive\n\u003e usb list\n\u003e usb import SETUP.DS               # a file you dragged on from your PC -\u003e /home/SETUP.DS\n\u003e usb sync                          # push all of /home onto the drive\n```\n\nNames are converted to 8.3 (`blink.ds` → `BLINK.DS`). Files dropped from the\nhost show up under their short name. To keep imported files across reboots, run\n`save` after importing.\n\n\u003e The host owns the filesystem while mounted. DeckOS only touches the disk in\n\u003e short interrupt-safe bursts, so reads/writes from both sides stay consistent.\n\n### USB keyboard (HID)\n\nThe board can act as a USB keyboard and type into the connected host -- handy for\nautomation, kiosk setup, or sending a fixed string on demand.\n\n| Command | What it does |\n|---|---|\n| `hid` / `hid status` | Show connection and ready state |\n| `hid type \u003ctext...\u003e` | Type the text into the host |\n| `hid line \u003ctext...\u003e` | Type the text, then press Enter |\n| `hid key \u003cCOMBO...\u003e` | Send one or more key combinations |\n| `hid enter` | Press Enter |\n\n`hid key` understands modifiers (`CTRL`, `ALT`, `SHIFT`, `GUI`/`WIN`/`CMD`) and\nnamed keys (`ENTER`, `TAB`, `ESC`, `SPACE`, `BACKSPACE`, `DEL`, `UP`, `DOWN`,\n`LEFT`, `RIGHT`, `HOME`, `END`, `F1`–`F12`, single letters/digits). Join a combo\nwith `+`.\n\n```bash\n\u003e hid type hello from DeckOS\n\u003e hid line echo \"typed by a microcontroller\"\n\u003e hid key WIN+r                    # open the Windows Run dialog\n\u003e hid key CTRL+ALT+DEL\n\u003e hid key F5                       # refresh\n```\n\n\u003e A device that can inject keystrokes is powerful -- only plug it into machines\n\u003e you trust, and remember it will start typing the moment a command runs.\n\n### Standalone handheld (OLED console)\n\nWith an SSD1306 OLED on GP4/GP5 and a battery on VSYS, DeckOS becomes a truly\nstandalone pocket computer: shell output is mirrored to the 128×64 display as a\nscrolling text console, so you don't need a host terminal to see what's\nhappening. (Input still comes from serial or the Bluetooth shell -- the RP2040's\nsingle USB port can't host a keyboard while also being a device.)\n\n| Command | What it does |\n|---|---|\n| `console` / `console status` | Show whether the OLED mirror is on |\n| `console oled on` | Mirror all shell output to the OLED (auto-inits the panel) |\n| `console oled off` | Stop mirroring |\n\n```bash\n\u003e console oled on\n\u003e sysinfo            # output now also appears on the OLED\n\u003e console oled off\n```\n\nThe console shows the last 8 lines (21 characters wide), swallows ANSI escape\ncodes so they don't appear as garbage, and redraws per line to keep I²C traffic\nlow. Pair it with the `power` command to monitor the battery.\n\n---\n\n## DeckScript\n\nDeckScript 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.\n\nScripts are plain text files with a `.ds` extension by convention. Comments start with `#`. Everything is line-based -- one statement per line.\n\n### Running scripts\n\n```bash\n# Write a script to VFS\n\u003e write /home/blink.ds led on\n\u003e append /home/blink.ds sleep 500\n\u003e append /home/blink.ds led off\n\u003e append /home/blink.ds sleep 500\n\n# Or open the built-in editor and type it properly\n\u003e edit /home/blink.ds\n\n# Or use the interactive write shell\n\u003e iwrite /home/blink.ds\n\n# Run it\n\u003e run /home/blink.ds\n\n# Or through the script command\n\u003e script run /home/blink.ds\n\n# Quick self-test\n\u003e script test\n```\n\n### Variables\n\nVariables are set with `let` and referenced with `$`. Variable names are alphanumeric plus underscores, up to 15 characters.\n\n```\nlet x = 10\nlet name = hello\nprint the value is $x\nprint hello $name\n```\n\nThere are two special variables available inside scripts:\n\n- **`$_i`** -- inside a `repeat` block, holds the current iteration number (0-based).\n- **`$return`** -- after a `call`, holds the value returned by the function (set via `let return = ...` or `return \u003cvalue\u003e` inside the function body).\n\n### Arithmetic\n\nBasic integer arithmetic in `let` expressions: `+`, `-`, `*`, `/`, `%`. The operator must be surrounded by spaces. Both sides can be a literal number or a `$variable` reference.\n\n```\nlet x = 10\nlet y = 3\nlet z = $x + $y\nprint $z\n\nlet r = $x % $y\nprint remainder is $r\n```\n\n### String functions\n\nThese all work as the right-hand side of a `let` statement. Variable references inside the arguments are expanded automatically.\n\n| Function | What it does |\n|---|---|\n| `upper(text)` | Convert to uppercase |\n| `lower(text)` | Convert to lowercase |\n| `len(text)` | Length of string |\n| `substr(text, start, length)` | Extract a substring |\n| `contains(haystack, needle)` | Returns `1` if needle found, `0` if not |\n| `trim(text)` | Strip leading and trailing whitespace |\n| `replace(text, old, new)` | Replace all occurrences of old with new |\n| `format(fmt, arg1, arg2, ...)` | Printf-style string formatting |\n\n```\nlet msg = hello world\nlet up = upper($msg)\nprint $up\n\nlet l = len($msg)\nprint length is $l\n\nlet piece = substr($msg, 6, 5)\nprint $piece\n\nlet result = format(%d degrees C, 27)\nprint $result\n```\n\n### Math functions\n\n| Function | What it does |\n|---|---|\n| `sqrt(x)` | Square root |\n| `pow(x, y)` | x to the power of y |\n| `abs(x)` | Absolute value |\n| `min(a, b)` | Smaller of two values |\n| `max(a, b)` | Larger of two values |\n| `clamp(x, lo, hi)` | Clamp x to the range [lo, hi] |\n| `map(x, in_lo, in_hi, out_lo, out_hi)` | Map a value from one range to another |\n| `rand(lo, hi)` | Random integer between lo and hi inclusive |\n| `avg(a, b, c, ...)` | Average of up to 5 values |\n\n```\nlet r = sqrt(144)\nprint $r\n\nlet val = clamp(150, 0, 100)\nprint $val\n\nlet mapped = map(512, 0, 1023, 0, 100)\nprint $mapped\n\nlet n = rand(1, 6)\nprint dice: $n\n```\n\n### Control flow\n\n```\nif $x == 10\n  print x is ten\nelif $x \u003e 10\n  print x is more than ten\nelse\n  print x is less than ten\nendif\n```\n\nComparison operators: `==`, `!=`, `\u003c`, `\u003e`, `\u003c=`, `\u003e=`. These work for both integers and strings (`\u003c`, `\u003e`, `\u003c=`, `\u003e=` are integer-only). A bare integer expression is truthy if non-zero; a non-empty string with no operator is always truthy.\n\n#### Switch\n\n```\nswitch $color\ncase red\n  print stop\ncase green\n  print go\ndefault:\n  print unknown color\nendswitch\n```\n\n#### Assert\n\n```\nassert $x == 10 or fail: x should be 10\n```\n\nIf the condition is false, the script prints the message and stops.\n\n### Loops\n\n#### repeat\n\n```\nrepeat 5\n  print iteration $_i\nendrepeat\n```\n\n`$_i` is 0-based and counts up to `n - 1`. `repeat` is capped at 10,000 iterations.\n\n#### while\n\n```\nlet i = 0\nwhile $i \u003c 10\n  print $i\n  let i = $i + 1\nendwhile\n```\n\nWhile loops are capped at 100,000 iterations as a safety net.\n\n#### for (range)\n\n```\nfor i from 1 to 10\n  print $i\nendfor\n\nfor i from 0 to 20 step 2\n  print $i\nendfor\n\nfor i from 10 to 1 step -1\n  print $i\nendfor\n```\n\n#### for (array)\n\n```\narr_new nums 0\narr_push nums 10\narr_push nums 20\narr_push nums 30\n\nfor val in nums\n  print $val\nendfor\n```\n\nYou can also use a variable that holds the array name:\n\n```\nlet myarr = nums\nfor val in $myarr\n  print $val\nendfor\n```\n\n#### break and continue\n\n```\nfor i from 1 to 10\n  if $i == 5\n    break\n  endif\n  print $i\nendfor\n```\n\n### Arrays\n\nArrays are dynamically sized. Elements are zero-indexed.\n\n```\narr_new scores 0       # create an empty array called scores\narr_push scores 95\narr_push scores 87\narr_push scores 72\n\nlet n = arr_len(scores)\nprint count: $n\n\nlet top = arr_get(scores, 0)\nprint top score: $top\n\narr_set scores 1 99    # overwrite element at index 1\n\narr_dump scores        # print all elements\n\narr_pop scores popped  # pop last element into variable 'popped'\nprint popped: $popped\n```\n\nYou can also pre-allocate with a size:\n\n```\narr_new buffer 10      # 10 elements, all \"0\"\narr_set buffer 3 hello\n```\n\n### Functions\n\nFunctions 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 = \u003cvalue\u003e` inside the function, or by using `return \u003cvalue\u003e` to set it and exit immediately.\n\n```\ndef greet\n  print hello $arg0\nenddef\n\ncall greet world\ncall greet DeckOS\n```\n\n```\ndef add\n  let return = $arg0 + $arg1\nenddef\n\ncall add 3 7\nprint result: $return\n```\n\nFunctions 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`.\n\n#### Recursion example\n\n```\ndef fact\n  if $arg0 \u003c= 1\n    let return = 1\n    return\n  endif\n  let n = $arg0\n  let p = $arg0 - 1\n  call fact $p\n  let return = $n * $return\nenddef\n\ncall fact 5\nprint $return\n```\n\n### Hardware access from scripts\n\nThis is where DeckScript gets useful for actual embedded work. All of these go on the right-hand side of a `let`.\n\n| Expression | What it does |\n|---|---|\n| `adc(0)` | Read ADC channel 0–2 (GP26–28), returns raw 12-bit value |\n| `gpio(15)` | Read the current state of GP15 (0 or 1) |\n| `pwm(16, 50)` | Set GP16 to 50% PWM duty cycle, returns 1 on success |\n| `millis` | Milliseconds since boot |\n| `micros` | Microseconds since boot |\n\n\u003e **Note:** `adc()` accepts channels 0, 1, and 2 only (GP26, GP27, GP28). Other channel numbers return 0.\n\nGPIO write and other hardware commands use shell commands directly inside the script:\n\n```\ngpio_write 15 1        # set GP15 high\ngpio_write 15 0        # set GP15 low\npulse 15 500           # high for 500 µs then low\nwait_pin 15 1 3000     # wait up to 3000 ms for GP15 to go high\nsleep 100              # wait 100 ms\n```\n\n`wait_pin` sets `$_timeout` to `1` if it times out before the pin changes.\n\n```\n# Read ADC and blink LED faster when value is high\nlet threshold = 2800\n\nrepeat 10\n  let raw = adc(0)\n  if $raw \u003e $threshold\n    gpio_write 25 1\n    sleep 100\n    gpio_write 25 0\n    sleep 100\n  else\n    gpio_write 25 1\n    sleep 500\n    gpio_write 25 0\n    sleep 500\n  endif\nendrepeat\n```\n\n### I/O\n\n```\nprint hello\nprint value is $x\nprintln same thing, also prints a newline\n\nlet name = input(what is your name? )\nprint nice to meet you $name\n\necho           # print a blank line\npause          # wait for any keypress before continuing\nvars           # dump all current variable names and values\n```\n\n### Logging and assertions\n\n```\nlog info myapp everything is fine\nlog warn myapp temperature is high\nlog err  myapp sensor read failed\nlog debug myapp raw = $raw\n\nassert $x \u003e 0 or fail: x must be positive\n```\n\nLog entries go into the system log and can be read with `syslog show`.\n\n### Includes\n\nScripts 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.\n\n```\ninclude /home/helpers.ds\n```\n\nYou can also call `run` from within a script to execute another script file. Unlike `include`, `run` creates a fresh variable context each time.\n\n```\nrun /home/tests/01_vars.ds\n```\n\n### Example scripts\n\n#### Blink the LED\n\n```\n# blink.ds - blink onboard LED 10 times\nrepeat 10\n  gpio_write 25 1\n  sleep 200\n  gpio_write 25 0\n  sleep 200\nendrepeat\nprint done\n```\n\n#### Read ADC and classify\n\n```\n# adc_check.ds\nlet raw = adc(0)\nlet mv = map($raw, 0, 4095, 0, 3300)\nprint raw: $raw\nprint voltage: $mv mV\n\nif $mv \u003e 2000\n  print HIGH\nelif $mv \u003e 1000\n  print MID\nelse\n  print LOW\nendif\n```\n\n#### Servo sweep with timing\n\n```\n# sweep.ds\nlet pin = 16\nlet step = 5\n\nfor angle from 0 to 180 step $step\n  servo $pin $angle\n  sleep 30\nendfor\n\nfor angle from 180 to 0 step -$step\n  servo $pin $angle\n  sleep 30\nendfor\n\nprint sweep complete\n```\n\n#### Simple function library\n\n```\n# lib.ds - reusable helpers\n\ndef wait_high\n  # wait for arg0 pin to go high, arg1 timeout ms\n  wait_pin $arg0 1 $arg1\n  if $_timeout == 1\n    print timeout waiting for pin $arg0\n  endif\nenddef\n\ndef blink_n\n  # blink pin arg0 n=arg1 times with arg2 ms delay\n  repeat $arg1\n    gpio_write $arg0 1\n    sleep $arg2\n    gpio_write $arg0 0\n    sleep $arg2\n  endrepeat\nenddef\n```\n\n---\n\n## Buzzer setup\n\nThe `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.\n\n**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.\n\nNot 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.\n\n**Wiring:**\n\n```\nGPIO pin ──── [100Ω resistor] ──── (+) Buzzer (−) ──── GND\n```\n\nAny GPIO pin works. GP16 is a good default. The 100 Ω resistor is optional but it protects the pin.\n\n**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.\n\n```bash\n\u003e melody 16 C4:200 D4:200 E4:200 F4:200 G4:200 A4:200 B4:200 C5:400\n\u003e tone 16 440 1000\n\u003e melody 16 elise\n```\n\n---\n\n## Config system\n\nSettings 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.\n\nChanges from `config set` are only in RAM until you run `config save`. CPU speed and LED state changes take effect on the next boot.\n\n---\n\n## Syslog\n\nDeckOS 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.\n\n```bash\n\u003e syslog show           # everything\n\u003e syslog show 20        # last 20 entries\n\u003e syslog warn           # WARN and above only\n\u003e syslog err            # errors only\n\u003e syslog write myapp \"something happened\"\n\u003e syslog stats\n\u003e syslog clear\n```\n\nLevels: `DBG` (grey) → `INF` (white) → `WRN` (yellow) → `ERR` (red).\n\n---\n\n## Scheduler\n\nBackground 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.\n\n```bash\n\u003e tasks                 # see all tasks\n\u003e tasks disable 0       # stop a task\n\u003e tasks enable 0        # bring it back\n\u003e top                   # watch live CPU usage per task\n\u003e jobs                  # list active background Core 1 jobs\n```\n\n---\n\n## Boot modes\n\n| Mode | How to trigger | What happens |\n|---|---|---|\n| **Normal** | Just power on | Full shell, all drivers loaded; USB enumerates as a composite CDC + MSC + HID device (see [USB portable OS](#usb-portable-os)) |\n| **DFU** | Run `dfu` in the shell | Jumps into USB bootloader so you can reflash |\n\nFor hardware recovery (e.g. after flashing a bad build), hold the RP2040's\n**BOOTSEL** button while plugging in USB -- the board mounts as the ROM\nmass-storage bootloader (a separate volume from the DeckOS `DECKOS` drive) so\nyou can drag a fresh `.uf2` onto it. The 1200-baud reset touch also still\nreboots the board into BOOTSEL. (Earlier versions used a GP15 recovery pin; that\nwas removed because it tied up a GPIO and could falsely trigger recovery mode on\nboards where the pin floats.)\n\n---\n\n## Drivers\n\nDrivers are registered and initialised in order at boot. Each one reports OK or FAIL.\n\n| Driver | What it covers |\n|---|---|\n| `adc` | ADC channels + internal temperature sensor |\n| `gpio` | GPIO (placeholder, the SDK handles most of this) |\n| `pwm` | PWM (placeholder) |\n| `i2c0` | I²C bus 0 at 100 kHz on GP4/GP5 |\n\n```bash\n\u003e drivers\n```\n\n---\n\n*Who doesn't love a decent shell?*","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenginestein%2Fdeckos","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fenginestein%2Fdeckos","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fenginestein%2Fdeckos/lists"}