Home›Plugin›Hardware›Firmware SDK
Firmware SDK
Build your own controller for Simulation Instruments Studio · line protocol
Your own switches, knobs, LEDs and displays can talk to Simulation Instruments Studio with nothing more than short lines of plain text. If a microcontroller can print a line over a serial port, it can be a Studio controller. No driver to install, no binary format to decode, and you can watch every message go by in a serial monitor while you debug.
- Use the board you like. The ready-made firmware covers ESP32, Arduino AVR boards and the Raspberry Pi Pico. This page is for everything else, or for when you want your own firmware.
- Same text everywhere. USB serial, Wi-Fi and Bluetooth LE carry exactly the same lines.
- Your instruments don’t change. Instrument scripts keep calling the hw_ functions. Studio turns those calls into the lines below.
If you just want to wire a board and go, use the ready-made firmware on the Hardware page. Everything here is taken from that firmware and from Studio itself.
On this page
- Line format
- Connections: USB, Wi-Fi, Bluetooth LE
- Connecting: HELLO, PINS, WHO and PING
- Messages from the board
- Messages to the board
- Message port payloads
- Saved settings: CFG, WIFI, HOST, BOARD
- How each hw_ function maps to lines
- Which boards support what
- Example sketch: one button and one LED
- Limits and known gaps
Line format
- Plain ASCII text, one message per line, ending in a newline (
\n). A carriage return (\r) before it is ignored. - Fields are separated by one space. The first field is the message name, in capitals.
- Pins are the board’s own GPIO numbers, written as plain integers (
BTN 32 1). - Empty lines are ignored. A line the board doesn’t understand should be ignored quietly.
Connections
USB serial (the normal way)
- 115200 baud, 8 data bits, no parity, 1 stop bit (8N1), no flow control. Studio opens the port with DTR and RTS off.
- Studio scans for new serial ports every 3 seconds. It accepts a port when the board sends
HELLOwithin about 1.2 seconds of opening it. If nothing arrives, Studio sendsWHOandPINGand accepts the port ifHELLOorPONGcomes back within about 2.5 seconds. Ports that don’t answer are skipped until they are unplugged. - Many boards reset when the port opens, so the simplest firmware just prints
HELLOat start-up. AnsweringWHOcovers boards that don’t reset. - On ESP32-S3 and ESP32-C3 boards with native USB, turn on USB CDC On Boot so the serial port is the USB port.
| Setting | What it does |
|---|---|
SIS_HW_BAUD | Baud rate Studio uses for every port (default 115200). |
SIS_HW_PORT | Use this one port. It is kept without waiting for HELLO. |
SIS_HW_USB=0 | Turns USB scanning off. |
These are environment variables, set before Studio starts.
Wi-Fi (TCP port 4731)
Wi-Fi is optional. Studio only uses it when the environment variable SIS_HW_WIFI=1 is set before it starts. The lines on the TCP connection are the same as on USB.
- Access-point mode (no network saved on the board). The ESP32 firmware opens its own open Wi-Fi network named
SIS-HWat192.168.4.1and listens on TCP port 4731. A computer on that network connects to192.168.4.1:4731and keeps the connection open, reconnecting every 3 seconds if it drops. The newest connection wins, so a quick setup connection also works.SIS_HW_HOSTandSIS_HW_HOST_PORTchange the address and port Studio connects to;SIS_HW_HOST=0turns this outgoing connection off. - Station mode (after
WIFIandHOSTwere saved). The board joins your cockpit network and connects out to the computer atHOST, port 4731, where Studio listens. If the connection drops, the firmware tries again every 3 seconds.SIS_HW_WIFI_PORTchanges the port Studio listens on. - Studio pings Wi-Fi boards that connected to it every 2 seconds and closes the link after 10 seconds without hearing anything.
- On a classic ESP32, analog inputs only read on ADC1 pins (GPIO 32 to 39) while Wi-Fi is running.
Bluetooth LE (Windows only for now)
- A Nordic UART style service, with no pairing: service
6E400001-B5A3-F393-E0A9-E50E24DCCA9E, RX6E400002-B5A3-F393-E0A9-E50E24DCCA9E(Studio writes here) and TX6E400003-B5A3-F393-E0A9-E50E24DCCA9E(the board sends notifications here). - The ESP32 firmware advertises as
SIS-plus the board name, with_and spaces turned into-, soESP32_BbecomesSIS-ESP32-B. - Same text lines as USB. Studio asks for an MTU of 247. The board sends lines in chunks of up to MTU − 3 bytes, and the receiver joins chunks and splits on newlines.
- Send
HELLOand thePINStable each time Studio subscribes to TX. - Studio reconnects remembered boards on its own and pings them every 2 seconds, closing the link after 10 seconds of silence.
SIS_HW_BLE=0turns Bluetooth off. - Bluetooth works in the Windows version of Studio only for now. Flashing always uses USB.
More than one connection to the same board
If a board with the same HELLO name is on several links, Studio uses USB first, then Bluetooth LE, then Wi-Fi. It sends only on the best link and ignores input from the others until that link goes away. So it’s fine for firmware to send every event on every link it has open.
Connecting: HELLO, PINS, WHO and PING
- When a connection opens, the board sends
HELLO <name> [version], then its pin table:PINS BEGIN, onePINS <line>per entry,PINS END. - The name is how instruments address the board (
name/12means GPIO 12 on that board). Use letters, digits and_, no spaces and no/. A second field that contains/(or looks likesis-xxx/1) is treated as a version, not a name. - After each
HELLO, Studio re-sends every runtime item the open instruments declared (inputs, outputs, displays, steppers, button arrays) and then sendsQUERY. Your firmware doesn’t need to remember them across restarts. A second identicalHELLOwithin 3 seconds is not treated as a reconnect. WHOasks the board to sendHELLOand itsPINStable again.PINGasks forPONG. Studio pings every board every 2 seconds. Answer on the link the request came from.
board: HELLO OVERHEAD my-fw/1
board: PINS BEGIN
board: PINS BTN 32
board: PINS SW 25
board: PINS END
app: IN 4
board: SW 4 0
app: QUERY
board: SW 25 1
board: BTN 32 0
app: PING
board: PONG
Messages from the board
| Line | Fields and values | Example |
|---|---|---|
HELLO <name> [version] | Sent on every new connection and in answer to WHO. | HELLO ESP32 sis-esp32/5-ble |
PINS BEGIN / PINS <line> / PINS END | The saved pin table, one CFG line per PINS line (see CFG). | PINS DIAL 26 27 14 |
BTN <gpio> <0|1> | Button up (0) or down (1), debounced. Pressed means the pin reads LOW. | BTN 32 1 |
SW <gpio> <0|1> | Switch pin off (0) or on (1), debounced. Also the answer to QUERY and IN. | SW 25 1 |
DIAL <gpio_a> <1|-1> | One detent of an encoder from the saved pin table, reported on pin A. | DIAL 26 -1 |
ENC <gpio_a> <1|-1> | One detent of an encoder added with ENC, reported on pin A. | ENC 16 1 |
ADC <gpio> <0..4095> | Analog value, 0 to 4095. Send it when it moves, and in answer to ADC and QUERY. Boards with a 10-bit converter scale to 0..4095. | ADC 34 2048 |
BA <id> <row> <col> <0|1> | A key in a button array went up (0) or down (1). Row and column start at 0. | BA 3 1 2 1 |
MSG <port> <msg_id> <type> <payload> | A message port message (see payloads). | MSG radio 7 INT 12 |
PONG | Answer to PING. | PONG |
OK | Network setup saved, the board is restarting (ESP32 setup mode). | OK |
ERR <text> | A request the board can’t do. Studio writes it to its log. | ERR DAC 4 has no DAC on this chip |
For BTN, SW, DIAL, ENC and ADC, Studio reads the second field as the pin and the third as a number. Lines with a non-numeric third field are ignored.
Messages to the board
Outputs
| Line | Fields and values | Example |
|---|---|---|
LED <gpio> <0..255> | 0 off, 255 full on, anything between is dimmed (PWM). The ESP32 firmware also treats 1 as full on. | LED 2 255 |
OUT <gpio> <0|1> | Digital output low or high. | OUT 13 1 |
PWM <gpio> <0..255> [hz] | Duty cycle 0..255 (8-bit). Frequency defaults to 5000 Hz on ESP32. Studio sends 1 to 40000 Hz (1000 if the script gives none). | PWM 18 128 1000 |
SRV <gpio> <pulse_us> | Hobby servo, 50 Hz frame, pulse 400 to 2600 microseconds. | SRV 13 1500 |
DAC <gpio> <0..255> | True analog output, 0 to 3.3 V. Classic ESP32 GPIO 25 and 26, ESP32-S2 GPIO 17 and 18. Answer ERR on other pins. | DAC 25 128 |
Inputs added at run time
| Line | Fields and values | Example |
|---|---|---|
IN <gpio> | Start watching a digital input (pull-up, active LOW). Report its state now and on every change as SW <gpio> <0|1>. | IN 4 |
ADC <gpio> | Start reporting an analog input. Report the value now. | ADC 34 |
ENC <a> <b> [steps] | Add a quadrature encoder. steps is valid transitions per detent, 1 to 8, default 4. Report ENC <a> <1|-1> per detent. Invalid jumps are ignored. | ENC 16 17 4 |
BA <id> ADD <rows> <cols> <row pins...> <col pins...> | Key matrix up to 8 by 8. Rows are driven LOW one at a time, columns read with pull-ups. Report BA lines. | BA 3 ADD 2 3 4 5 12 13 14 |
QUERY [gpio] | Report the current state of one pin, or of every input: SW, BTN and ADC lines. | QUERY 25 |
Character displays
| Line | Fields and values | Example |
|---|---|---|
CHR <id> ADD <type> <n> <din> <clk> <cs> | A chain of n displays (1 to 8). The ready-made firmware supports MAX7219 8-digit 7-segment modules only and answers ERR for other types. | CHR 1 ADD MAX7219 2 23 18 5 |
CHR <id> TXT <display> <line> <text> | Text for one display in the chain (0-based). The text is the rest of the line and may contain spaces. It uses the same escaping as MSG. | CHR 1 TXT 0 0 118.50 |
CHR <id> BRT <display> <0..15> | Brightness. 0 turns the display off. | CHR 1 BRT 0 8 |
On MAX7219 the ready-made firmware can show digits 0 to 9, -, E, H, L, P, blank and .; other characters show blank. Text is left-aligned.
Stepper motors
| Line | Fields and values | Example |
|---|---|---|
STEP <id> ADD <type> <steps_per_rev> <rpm> <pins...> [home] | Types containing STEP, A4988, DRV8825 or TMC (for example STEP_DIR): pins step, dir. Types ULN2003, 28BYJ, 4WIRE, VID29, X27: pins in1 to in4 (full-step). An extra pin is a home switch to ground. Unknown types: 4 or more pins means 4-wire, otherwise step/dir. A driver board is always needed. | STEP 2 ADD X27 720 10 12 13 14 15 |
STEP <id> POS <steps> <CW|CCW|FASTEST|DIRECT> | Move. CW, CCW and FASTEST wrap within one revolution. DIRECT goes to the absolute step count. Default FASTEST. | STEP 2 POS 360 FASTEST |
STEP <id> CAL <steps> | With a home switch: turn until it closes (up to two revolutions), then call that position steps. Without one: the current position becomes steps. | STEP 2 CAL 0 |
STEP <id> RPM <rpm> | Speed in revolutions per minute. | STEP 2 RPM 20 |
Connection and setup
| Line | What the board does |
|---|---|
PING | Answers PONG. |
WHO | Answers HELLO and its PINS table. |
MSG <port> <msg_id> <type> <payload> | Delivers a message port message to your code. |
CFG ..., WIFI ..., HOST ..., BOARD ... | Saved settings, below. |
The ids in CHR, STEP and BA are numbers Studio picks; they are unique while Studio runs. IN, ADC, DAC, PWM, SRV, OUT, CHR, STEP and BA are not saved on the board; Studio sends them again after every HELLO.
Message port payloads
MSG <port> <msg_id> <type> <payload> works in both directions. port is the port name (no spaces), msg_id an integer, and the payload is everything after the fourth space, so it may contain spaces.
Escaping: %, carriage return and newline are sent as %25, %0D and %0A. Undo it when reading (lower-case %0a and %0d are accepted too).
| Type | Payload | What the instrument gets |
|---|---|---|
INT, BYTE, FLOAT | One number, with a dot for decimals. | A number. Studio also accepts INT32 and DOUBLE from the board. |
STRING | Text. | A string. Any type Studio doesn’t know also arrives as a string. |
INT[], BYTE[], FLOAT[] | Numbers separated by commas. | A table of numbers. |
NIL | Empty. | nil. A missing type is treated as NIL. |
When an instrument sends, Studio writes the type in capitals (default STRING). True and false become 1 and 0, and a table becomes its values joined by commas. Example: MSG radio 7 STRING COM1%0A118.50.
Saved settings: CFG, WIFI, HOST and BOARD
These lines are kept in the board’s own storage and survive a restart. Studio sends them from the Hardware tab.
| Line | What it does |
|---|---|
CFG CLEAR | Starts a new pin table. |
CFG BTN <pin> | A button, reported as BTN. |
CFG SW <pin> | A switch, reported as SW. |
CFG DIAL <a> <b> [sw] | An encoder reported as DIAL on pin A (4 transitions per detent), with an optional push button reported as BTN. |
CFG END | Saves the table, applies it and sends the PINS table back. |
WIFI <ssid> <password> | Saves the network. The network name ends at the first space; the password is the rest of the line. |
HOST <ip> | Saves the computer’s address the board connects to on port 4731, then reconnects. In access-point mode, once WIFI and HOST are both saved, the board sends OK and restarts into station mode. |
BOARD <name> | Saves the board name and sends HELLO again under the new name on every open link. The Bluetooth name changes after a restart. |
Studio sends a pin table as BOARD (when a name is set), CFG CLEAR, one CFG line each, then CFG END. Setup over the access point sends WIFI, HOST and BOARD on one connection. All saved inputs use the internal pull-up and are active LOW.
How each hw_ function maps to lines
A pin written name/12 goes only to the board that said HELLO name; a bare 12 goes to every board. The board name is removed before the line is sent.
| Function | Studio sends | Studio listens for |
|---|---|---|
hw_button_add | Nothing; QUERY on reconnect. The pin comes from the board’s own table. | BTN (or any line) for the pin; above 0.5 is pressed. |
hw_switch_add | QUERY <gpio> for each pin. | Lines for its pins. One pin: position 0 or 1. Several pins: position is the index of the first closed pin. |
hw_switch_get_position | Nothing; reads the last value. | — |
hw_dial_add | Nothing. The pins come from the board’s own table. | DIAL or ENC on pin A, one callback per detent. |
hw_dial_set_acceleration | Nothing; applied inside Studio (1 to 20, 1 is off). | — |
hw_input_add / hw_input_read | IN <gpio>, then QUERY <gpio>. | The pin’s state, as true or false. |
hw_led_add / hw_led_set | LED <gpio> <brightness × 255> | — |
hw_output_add / hw_output_set | OUT <gpio> <0|1> | — |
hw_output_pwm_add / hw_output_pwm_duty_cycle | PWM <gpio> <duty × 255> <hz> | — |
hw_servo_add / hw_servo_set_position | SRV <gpio> <pulse_us>. Position 0.0 to 1.0 maps onto the minimum and maximum pulse (default 1000 and 2000). | — |
hw_adc_input_add / hw_adc_input_read | ADC <gpio> | ADC; the value is divided by 4095 to give 0.0 to 1.0. |
hw_dac_output_add / hw_dac_output_set | DAC <gpio> <value × 255> | — |
hw_chr_display_add | CHR <id> ADD <type> <n> <pins> | — |
hw_chr_display_set_text | CHR <id> TXT <display> <line> <text> | — |
hw_chr_display_set_brightness | CHR <id> BRT <display> <brightness × 15> | — |
hw_stepper_motor_add | STEP <id> ADD <type> <steps> <rpm> <pins> | — |
hw_stepper_motor_position | STEP <id> POS <position × steps> <dir>. Position is a fraction of one turn. | — |
hw_stepper_motor_calibrate | STEP <id> CAL <position × steps> | — |
hw_stepper_motor_speed | STEP <id> RPM <rpm> | — |
hw_button_array_add | BA <id> ADD <rows> <cols> <pins> | BA <id> <row> <col> <0|1>; callbacks get row and column. |
hw_message_port_add | Nothing. | MSG <port> ...; the callback gets the message id and payload. |
hw_message_port_send | MSG <port> <msg_id> <type> <payload> | — |
hw_connected | Nothing; true when the pin’s board is connected. | — |
Pin names such as ESP32_GPIO_27, GPIO27 or D2 become the plain number at the end. The full call reference is on the hw_ functions page.
Which boards support what
| Lines | ESP32 | Arduino AVR | Pico / Pico 2 |
|---|---|---|---|
HELLO, PINS, WHO, PING/PONG, QUERY, ERR | Yes | Yes (empty PINS table) | Yes (empty PINS table) |
IN | Yes, reports SW | Yes, reports BTN | Yes, reports BTN |
ADC | Yes, 12-bit | Yes, 10-bit scaled to 0..4095 | Yes, 10-bit scaled to 0..4095 |
ENC, BA | Yes | Yes | Yes |
LED, OUT, PWM | Yes | Yes (PWM frequency fixed) | Yes (PWM frequency fixed) |
SRV, DAC, CHR, STEP, MSG | Yes | No | No |
CFG (BTN/SW/DIAL table) | Yes | No | No |
WIFI, HOST, BOARD | Yes | No | No |
| Wi-Fi, Bluetooth LE | Yes | No (USB only) | No (USB only) |
CHANNEL <A..Z>, ID | No | Yes | Yes |
The AVR and Pico firmware name themselves after the board type and a channel letter, for example HELLO ARDUINO_NANO_A sis-avr/1, followed by ID ARDUINO_NANO A. CHANNEL B changes the letter, saves it and sends HELLO again. Lines a board doesn’t support are ignored.
Example sketch: one button and one LED
An Arduino-style sketch for any board with a USB serial port. A button from GPIO 4 to ground is reported as BTN 4, and GPIO 2 follows LED and OUT. The board calls itself MYPANEL.
// Minimal Simulation Instruments Studio controller: one button, one LED.
const char *NAME = "MYPANEL";
const int BUTTON = 4; // to ground, internal pull-up
const int LED_PIN = 2;
bool down = false, raw = false;
unsigned long since = 0;
String line;
void hello() {
Serial.print("HELLO "); Serial.print(NAME); Serial.println(" my-fw/1");
Serial.println("PINS BEGIN");
Serial.print("PINS BTN "); Serial.println(BUTTON);
Serial.println("PINS END");
}
void report() {
Serial.print("BTN "); Serial.print(BUTTON); Serial.print(' '); Serial.println(down ? 1 : 0);
}
void handle(const String &s) {
int pin, value;
if (s == "PING") { Serial.println("PONG"); return; }
if (s == "WHO") { hello(); return; }
if (s == "QUERY" || s == "QUERY " + String(BUTTON)) { report(); return; }
if (sscanf(s.c_str(), "LED %d %d", &pin, &value) == 2 || sscanf(s.c_str(), "OUT %d %d", &pin, &value) == 2) {
if (pin == LED_PIN) { pinMode(LED_PIN, OUTPUT); digitalWrite(LED_PIN, value ? HIGH : LOW); }
}
// Anything else is ignored.
}
void setup() {
Serial.begin(115200);
pinMode(BUTTON, INPUT_PULLUP);
down = raw = digitalRead(BUTTON) == LOW;
delay(100);
hello();
}
void loop() {
while (Serial.available()) {
char c = Serial.read();
if (c == '\r') continue;
if (c != '\n') { if (line.length() < 200) line += c; continue; }
line.trim();
if (line.length()) handle(line);
line = "";
}
bool now = digitalRead(BUTTON) == LOW; // debounce: must hold 20 ms
if (now != raw) { raw = now; since = millis(); }
else if (now != down && millis() - since >= 20) { down = now; report(); }
}
In an instrument:
local led = hw_led_add("MYPANEL/2", 0)
hw_button_add("MYPANEL/4",
function() hw_led_set(led, 1) end,
function() hw_led_set(led, 0) end)
Limits and known gaps
- Bluetooth LE works in the Windows version of Studio only for now.
- No UDP or WebSocket. The connections are USB serial, TCP 4731 and Bluetooth LE.
- Character displays: the ready-made firmware drives MAX7219 7-segment modules only (up to 2 chains of 8). Character LCDs, TM1637 and I2C displays need your own firmware. The
linefield inCHR TXTis sent, but the ready-made firmware shows one line per display. - Buttons, switches and dials declared with
hw_button_add,hw_switch_addorhw_dial_addare not announced to the board. The board has to report those pins on its own, for example from its savedCFGtable. - LED lines and stepper
CALlines are not sent again after a reconnect; the other outputs are. - Line length: the ESP32 firmware keeps up to 400 characters per line, the AVR and Pico firmware 95. Keep lines short.
- Table sizes in the ESP32 firmware: 16 buttons, 8 switches, 8 dials, 8 encoders, 8 analog inputs, 2 displays, 4 steppers, 2 button arrays. AVR and Pico: 48 inputs, 16 analog inputs, 8 encoders, 2 button arrays.
- Analog change threshold: the ESP32 firmware reports a move of 16 or more (checked every 20 ms); AVR and Pico report a move of more than 8.
- Wi-Fi network names can’t contain spaces in
WIFI; passwords can. - There is no checksum or acknowledgement. Studio relies on
PING/PONG, re-sending afterHELLOandQUERYto stay in step.