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.

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

  1. Line format
  2. Connections: USB, Wi-Fi, Bluetooth LE
  3. Connecting: HELLO, PINS, WHO and PING
  4. Messages from the board
  5. Messages to the board
  6. Message port payloads
  7. Saved settings: CFG, WIFI, HOST, BOARD
  8. How each hw_ function maps to lines
  9. Which boards support what
  10. Example sketch: one button and one LED
  11. Limits and known gaps

Line format

Connections

USB serial (the normal way)

SettingWhat it does
SIS_HW_BAUDBaud rate Studio uses for every port (default 115200).
SIS_HW_PORTUse this one port. It is kept without waiting for HELLO.
SIS_HW_USB=0Turns 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.

Bluetooth LE (Windows only for now)

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

  1. When a connection opens, the board sends HELLO <name> [version], then its pin table: PINS BEGIN, one PINS <line> per entry, PINS END.
  2. The name is how instruments address the board (name/12 means GPIO 12 on that board). Use letters, digits and _, no spaces and no /. A second field that contains / (or looks like sis-xxx/1) is treated as a version, not a name.
  3. After each HELLO, Studio re-sends every runtime item the open instruments declared (inputs, outputs, displays, steppers, button arrays) and then sends QUERY. Your firmware doesn’t need to remember them across restarts. A second identical HELLO within 3 seconds is not treated as a reconnect.
  4. WHO asks the board to send HELLO and its PINS table again. PING asks for PONG. 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

LineFields and valuesExample
HELLO <name> [version]Sent on every new connection and in answer to WHO.HELLO ESP32 sis-esp32/5-ble
PINS BEGIN / PINS <line> / PINS ENDThe 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
PONGAnswer to PING.PONG
OKNetwork 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

LineFields and valuesExample
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

LineFields and valuesExample
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

LineFields and valuesExample
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

LineFields and valuesExample
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

LineWhat the board does
PINGAnswers PONG.
WHOAnswers 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).

TypePayloadWhat the instrument gets
INT, BYTE, FLOATOne number, with a dot for decimals.A number. Studio also accepts INT32 and DOUBLE from the board.
STRINGText.A string. Any type Studio doesn’t know also arrives as a string.
INT[], BYTE[], FLOAT[]Numbers separated by commas.A table of numbers.
NILEmpty.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.

LineWhat it does
CFG CLEARStarts 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 ENDSaves 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.

FunctionStudio sendsStudio listens for
hw_button_addNothing; 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_addQUERY <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_positionNothing; reads the last value.—
hw_dial_addNothing. The pins come from the board’s own table.DIAL or ENC on pin A, one callback per detent.
hw_dial_set_accelerationNothing; applied inside Studio (1 to 20, 1 is off).—
hw_input_add / hw_input_readIN <gpio>, then QUERY <gpio>.The pin’s state, as true or false.
hw_led_add / hw_led_setLED <gpio> <brightness × 255>—
hw_output_add / hw_output_setOUT <gpio> <0|1>—
hw_output_pwm_add / hw_output_pwm_duty_cyclePWM <gpio> <duty × 255> <hz>—
hw_servo_add / hw_servo_set_positionSRV <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_readADC <gpio>ADC; the value is divided by 4095 to give 0.0 to 1.0.
hw_dac_output_add / hw_dac_output_setDAC <gpio> <value × 255>—
hw_chr_display_addCHR <id> ADD <type> <n> <pins>—
hw_chr_display_set_textCHR <id> TXT <display> <line> <text>—
hw_chr_display_set_brightnessCHR <id> BRT <display> <brightness × 15>—
hw_stepper_motor_addSTEP <id> ADD <type> <steps> <rpm> <pins>—
hw_stepper_motor_positionSTEP <id> POS <position × steps> <dir>. Position is a fraction of one turn.—
hw_stepper_motor_calibrateSTEP <id> CAL <position × steps>—
hw_stepper_motor_speedSTEP <id> RPM <rpm>—
hw_button_array_addBA <id> ADD <rows> <cols> <pins>BA <id> <row> <col> <0|1>; callbacks get row and column.
hw_message_port_addNothing.MSG <port> ...; the callback gets the message id and payload.
hw_message_port_sendMSG <port> <msg_id> <type> <payload>—
hw_connectedNothing; 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

LinesESP32Arduino AVRPico / Pico 2
HELLO, PINS, WHO, PING/PONG, QUERY, ERRYesYes (empty PINS table)Yes (empty PINS table)
INYes, reports SWYes, reports BTNYes, reports BTN
ADCYes, 12-bitYes, 10-bit scaled to 0..4095Yes, 10-bit scaled to 0..4095
ENC, BAYesYesYes
LED, OUT, PWMYesYes (PWM frequency fixed)Yes (PWM frequency fixed)
SRV, DAC, CHR, STEP, MSGYesNoNo
CFG (BTN/SW/DIAL table)YesNoNo
WIFI, HOST, BOARDYesNoNo
Wi-Fi, Bluetooth LEYesNo (USB only)No (USB only)
CHANNEL <A..Z>, IDNoYesYes

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