Dev boards¶
Status: prototype. This is new and incomplete: its commands, files and behavior may change or be removed in any release.
A dev board starter turns a new board into a device termapy drives on day
one. It holds the code that runs on the board, plus a host config and a
device profile. The board answers at an interactive prompt, its commands
show in /help, and an MCP agent gets them as typed tools. This is real
hardware; the simulator is /demo.
| Board | Runs | Device code | Board commands |
|---|---|---|---|
pico |
MicroPython | main.py, termapy_json.py |
help, led, temp, id, json |
esp32s3 |
MicroPython | main.py, termapy_json.py, termapy_mem.py |
help, led, led_rgb, temp, id, json, MEM.* |
/board the boards, and which is plugged in now
/board.firmware pico copy the board's device code here, for flashing
/board.new pico create the config and profile for the attached board
/cfg.load pico switch to it
termapy --board pico the same from the command line: create once, then start it
The config connects by the board's USB serial number, so a COM number
that moves between plug-ins does not matter, and the same config works on
Windows, Linux and macOS. /board.new finds the board by its USB VID:PID.
When several are attached it takes the first and says how many it skipped.
With none attached it says whether the board is missing or attached but
not running MicroPython yet.
Everything for the board lands in its config's folder: the config, its
profile, and the device code in firmware/, so a new board is one folder
under termapy_cfg/ and nothing in the folder you started termapy from.
Board setup never replaces anything. A config by the same name stops it,
and so does any config that already connects to the attached board:
/board.new names that config instead of making a second one, and
termapy --board starts it. termapy does not write to the board itself;
flashing it and copying the device code onto it are your steps.
Raspberry Pi Pico¶
The starter runs on MicroPython, so the first time takes three steps and no compiler:
- Flash MicroPython. Hold BOOTSEL while plugging the board in; it
appears as a drive named
RPI-RP2. Download theRPI_PICO.uf2from micropython.org and drag it onto the drive. The board restarts as a USB serial port. - Copy the device code.
/board.firmware picowritesmain.pyandtermapy_json.pyto the active config'sfirmware/folder and prints the one command that puts them on the board, with the board's port and the files' full paths:uvx mpremote connect COM5 cp ...\main.py ...\termapy_json.py : + reset. MicroPython then runsmain.pyat every power-up. - Create the config.
/board.new pico, then/cfg.load pico, or start termapy withtermapy --board pico.
The on-chip sensor reads a few degrees off and is noisy; the RP2040 datasheet says as much.
ESP32-S3 DevKitC-1¶
The board has two USB ports. Use the one wired to the chip's own USB (usually marked USB): there the board has a unique serial number. The other port goes through a USB-to-UART bridge chip, which reports its own maker and no serial number, so nothing on it says which board it is.
- Flash MicroPython. Download
ESP32_GENERIC_S3from micropython.org: the SPIRAM_OCT build for a module with octal PSRAM (N8R8, N16R8; esptool shows it as "Embedded PSRAM 8MB"), the plain one otherwise. Then, with the port the chip's USB shows now:
uvx esptool --chip esp32s3 --port COM8 erase-flash
uvx esptool --chip esp32s3 --port COM8 write-flash 0 ESP32_GENERIC_S3-SPIRAM_OCT-<date>-v<version>.bin
The board restarts as a new serial port, MicroPython's (303A:4001).
2. Copy the device code. /board.firmware esp32s3, then the
uvx mpremote line it prints, as on the Pico; the S3 has a third file,
termapy_mem.py.
3. Create the config. /board.new esp32s3, then /cfg.load esp32s3,
or start termapy with termapy --board esp32s3.
The board's RGB LED on GPIO48 is a red, a green and a blue LED in one
package, each with its own level. led switches it as on the Pico: on
is a dim white, every level 32. led_rgb sets the three levels:
led_rgb 255 0 0 is full red, each level 0 to 255 in decimal or 0x hex.
A level out of range is refused, not clipped. Bare led_rgb reports the
levels: [255,0,0] in JSON, one level per line in text. A board with its
LED on another pin changes one line of main.py. id is the chip's
factory MAC address. MicroPython answers on the UART port too, but the
config uses the chip's own USB, by its serial number.
The S3 also answers the MEM commands that /mem.* speaks (Memory),
from termapy_mem.py: MEM.R, MEM.W, MEM.M and MEM.INFO. The board
reads and writes whatever address it is given and knows nothing about its
memory; checking the address is termapy's job, from the JSON that
describes the part. termapy does not check yet, so for now an address the
chip cannot reach resets it. A board that must fence memory off passes a
guard to direct() in main.py: guard(addr, count, write) sees every
access first, and returning False refuses it with ERR range. The MEM
replies are the wire spec's text even when the board answers MCP in JSON,
so /mem.* works the same in the TUI and over MCP.
The board's commands¶
Each board answers one command per line:
| Command | Text reply | JSON reply |
|---|---|---|
help |
one name help line per command |
{"result":{"led":"the onboard LED...",...}} |
help <command> |
that command's help line | {"result":"the on-chip temperature in degrees C"} |
led |
the LED's state | {"result":"on"} |
led on\|off\|1\|0 |
the new state, and the LED follows | {"result":"off"} |
led_rgb |
ESP32-S3: the levels, one per line | {"result":[255,0,0]} |
led_rgb <r> <g> <b> |
ESP32-S3: the new levels | {"result":[0,0,32]} |
temp |
degrees C, such as 21.4 |
{"result":21.4}, a number |
id |
the board's unique ID in hex | {"result":"E66548545710822C"} |
json {on\|off} |
the framing, on or off |
{"result":"on"} |
MEM.R, MEM.W, MEM.M, MEM.INFO |
ESP32-S3: the memory wire spec's rows, OK or ERR |
the same text: MEM keeps its own |
The board speaks two framings. Text, the default, is for a person: a reply
is the value, and an error is ERR <reason>. JSON is for termapy's MCP
server and any JSON-RPC client: each reply is one JSON-RPC 2.0 response
line, the same contract as the C module on JSON integration,
with an error as {"error":{"code":...,"message":...}}. json on switches
to it, and a request line such as {"method":"temp","id":1} switches by
itself and gets its id back. Each board's config sends json on when
the MCP server connects, so an agent gets typed values while the TUI stays
in text.
Each command in main.py is a row of its COMMANDS table: the name, a
one-line help and the handler, which takes the argument words and returns
the reply value. help reads its answer from that table, so a new command
is a handler and a row, and help lists it at once. termapy_json.py is
the monitor itself, one file every MicroPython board shares.
The board's profile describes every command in this table and every form
of it, help and json included, so /help shows them all and an MCP
agent can call each one, its arguments checked before they are sent. A new
command also gets its profile entry; the starter's tests fail until it has
one.
Ctrl-C in the terminal stops the monitor and leaves the MicroPython >>>
prompt; Ctrl-D restarts it.