Robulus Code Writing Guide
Audience: Robot builders, LLMs writing firmware code, and anyone creating user logic for a Robulus unit in Juvantia Fabrica.
1. How Robulus Firmware Works
The ESP32-S3 has two CPU cores. Juvantia splits them:
| Core | Owner | Runs | You Can Modify? |
|---|---|---|---|
| Core 0 | System | Wi-Fi, WebSocket, heartbeat, command parsing, OTA | ❌ No |
| Core 1 | User | Your code — user_setup(), user_loop(), deck() | ✅ Yes |
Execution Flow
POWER ON
│
├─── Core 0: System boots
│ • Connects to Wi-Fi (hardcoded park / user-configured home network)
│ • Opens WebSocket to deck.juvantia.org
│ • Starts heartbeat (every 5s)
│ • Waits for commands from Deck Control
│
└─── Core 1: User code boots
• Calls user_setup() ONCE
• Enters infinite loop:
while(true) {
user_loop(); // your continuous logic
yield(); // allow system tasks
}
WHEN DECK CONTROL SENDS A COMMAND:
Core 0 receives it via WebSocket
Core 0 parses it: { type: "AXIS", name: "Drive", value: 75 }
Core 0 calls → deck("AXIS", "Drive", 75) on Core 1
Your code handles it2. The Three Functions You Must Implement
Your code file must contain exactly these three functions. The system expects them. You can add anything else on top of them.
2.1 user_setup()
void user_setup() {
// Called ONCE when Core 1 starts
// Use for: pinMode(), library init, variable defaults
}Purpose: Initialize your hardware. This runs once after boot.
What to do here:
- Call
pinMode(pin, OUTPUT)orpinMode(pin, INPUT)for your GPIOs - Initialize libraries:
FastLED.addLeds(...),servo.attach(pin),Wire.begin() - Set initial states:
digitalWrite(pin, LOW), variables to zero
Example:
#include <FastLED.h>
#include <ESP32Servo.h>
#define PIN_DRIVE 5
#define PIN_STEER 6
#define PIN_HEADLIGHTS 12
#define PIN_LEDS 48
#define NUM_LEDS 30
CRGB leds[NUM_LEDS];
Servo turretServo;
bool headlightsOn = false;
void user_setup() {
// Motor pins
pinMode(PIN_DRIVE, OUTPUT);
pinMode(PIN_STEER, OUTPUT);
// Headlights
pinMode(PIN_HEADLIGHTS, OUTPUT);
digitalWrite(PIN_HEADLIGHTS, LOW);
// LED strip
FastLED.addLeds<WS2812, PIN_LEDS, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(80);
// Servo
turretServo.attach(9);
turretServo.write(90); // center position
}2.2 user_loop()
void user_loop() {
// Called REPEATEDLY in an infinite loop on Core 1
// Use for: animations, sensor reads, timed events, PID
}Purpose: Continuous logic. Runs thousands of times per second.
CRITICAL RULES:
- ❌ NEVER use
delay()— it blocks the entire Core 1 and can interfere with command processing - ✅ Use
millis()for all timing (see examples below) - Keep each iteration fast (< 10ms ideally)
Example — LED Animation + Periodic Sensor Read:
unsigned long lastSensorRead = 0;
uint8_t hue = 0;
void user_loop() {
// Rainbow animation — runs every loop
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(hue + (i * 10), 255, 255);
}
hue++;
FastLED.show();
// Read temperature sensor every 2 seconds (non-blocking)
if (millis() - lastSensorRead > 2000) {
lastSensorRead = millis();
float temp = readTemperature(); // your function
// do something with temp
}
}Example — Non-Blocking Timing (replacing delay):
// ❌ BAD — blocks everything for 1 second:
void user_loop() {
digitalWrite(LED, HIGH);
delay(1000); // NEVER DO THIS
digitalWrite(LED, LOW);
delay(1000); // NEVER DO THIS
}
// ✅ GOOD — non-blocking blink:
unsigned long lastBlink = 0;
bool ledState = false;
void user_loop() {
if (millis() - lastBlink > 1000) {
lastBlink = millis();
ledState = !ledState;
digitalWrite(LED, ledState);
}
}2.3 deck()
void deck(String type, String name, int value) {
// Called by the SYSTEM when Deck Control sends a command
// type: "AXIS" or "BUTTON"
// name: the control name you configured (e.g., "Drive", "Headlights")
// value: AXIS → -100 to +100 / BUTTON → 0 or 1
}Purpose: React to real-time commands from Deck Control.
Parameters:
| Parameter | Type | Values | Meaning |
|---|---|---|---|
type | String | "AXIS" or "BUTTON" | What kind of control sent the command |
name | String | The name you gave in Step 2 | Which specific control (e.g., "Drive", "Horn") |
value | int | -100 to +100 (axis) or 0/1 (button) | The control's current value |
How value works for axes:
Joystick fully back / Key "S": value = -100
Joystick centered / No key: value = 0
Joystick fully forward / Key "W": value = +100
Joystick halfway forward: value = 50How value works for buttons:
Momentary mode:
Key pressed: value = 1
Key released: value = 0
Toggle mode:
First click: value = 1
Second click: value = 0Full Example:
void deck(String type, String name, int value) {
// ─── AXES ─────────────────────────────────────
if (type == "AXIS") {
if (name == "Drive") {
// value: -100 (full reverse) to +100 (full forward)
if (value > 0) {
// Forward
digitalWrite(IN1, HIGH);
digitalWrite(IN2, LOW);
analogWrite(PIN_DRIVE, map(value, 0, 100, 0, 255));
} else if (value < 0) {
// Reverse
digitalWrite(IN1, LOW);
digitalWrite(IN2, HIGH);
analogWrite(PIN_DRIVE, map(abs(value), 0, 100, 0, 255));
} else {
// Stop
digitalWrite(IN1, LOW);
digitalWrite(IN2, LOW);
analogWrite(PIN_DRIVE, 0);
}
}
if (name == "Steering") {
// Map -100..+100 to servo angle 0..180
int angle = map(value, -100, 100, 0, 180);
turretServo.write(angle);
}
}
// ─── BUTTONS ──────────────────────────────────
if (type == "BUTTON") {
if (name == "Headlights") {
// value: 1 = ON, 0 = OFF (toggle mode)
headlightsOn = (value == 1);
digitalWrite(PIN_HEADLIGHTS, headlightsOn ? HIGH : LOW);
}
if (name == "Horn") {
// value: 1 = pressed, 0 = released (momentary mode)
digitalWrite(PIN_HORN, value ? HIGH : LOW);
}
if (name == "Boost") {
// Momentary: while held, increase speed by 50%
boostActive = (value == 1);
}
}
}3. Your Code Structure
Minimal Valid Code
void user_setup() {
// empty is fine
}
void user_loop() {
// empty is fine
}
void deck(String type, String name, int value) {
// empty is fine — robot just ignores all commands
}Full Typical Structure
// ═══════════════════════════════════════════
// INCLUDES
// ═══════════════════════════════════════════
#include <FastLED.h>
#include <ESP32Servo.h>
// ═══════════════════════════════════════════
// PIN DEFINITIONS (from Step 2)
// ═══════════════════════════════════════════
#define PIN_MOTOR_A_PWM 5
#define PIN_MOTOR_A_DIR1 6
#define PIN_MOTOR_A_DIR2 7
#define PIN_MOTOR_B_PWM 8
#define PIN_MOTOR_B_DIR1 9
#define PIN_MOTOR_B_DIR2 10
#define PIN_HEADLIGHTS 12
#define PIN_HORN 13
#define PIN_LED_STRIP 48
#define NUM_LEDS 30
// ═══════════════════════════════════════════
// GLOBAL STATE
// ═══════════════════════════════════════════
CRGB leds[NUM_LEDS];
Servo cameraServo;
bool headlightsOn = false;
bool boostActive = false;
int currentSpeed = 0;
int currentSteering = 0;
unsigned long lastStatusReport = 0;
uint8_t animHue = 0;
// ═══════════════════════════════════════════
// HELPER FUNCTIONS (yours — add as many as you want)
// ═══════════════════════════════════════════
void setMotorA(int speed) {
// speed: -255 to +255
if (speed > 0) {
digitalWrite(PIN_MOTOR_A_DIR1, HIGH);
digitalWrite(PIN_MOTOR_A_DIR2, LOW);
analogWrite(PIN_MOTOR_A_PWM, speed);
} else if (speed < 0) {
digitalWrite(PIN_MOTOR_A_DIR1, LOW);
digitalWrite(PIN_MOTOR_A_DIR2, HIGH);
analogWrite(PIN_MOTOR_A_PWM, -speed);
} else {
digitalWrite(PIN_MOTOR_A_DIR1, LOW);
digitalWrite(PIN_MOTOR_A_DIR2, LOW);
analogWrite(PIN_MOTOR_A_PWM, 0);
}
}
void setMotorB(int speed) {
// same pattern as Motor A
if (speed > 0) {
digitalWrite(PIN_MOTOR_B_DIR1, HIGH);
digitalWrite(PIN_MOTOR_B_DIR2, LOW);
analogWrite(PIN_MOTOR_B_PWM, speed);
} else if (speed < 0) {
digitalWrite(PIN_MOTOR_B_DIR1, LOW);
digitalWrite(PIN_MOTOR_B_DIR2, HIGH);
analogWrite(PIN_MOTOR_B_PWM, -speed);
} else {
digitalWrite(PIN_MOTOR_B_DIR1, LOW);
digitalWrite(PIN_MOTOR_B_DIR2, LOW);
analogWrite(PIN_MOTOR_B_PWM, 0);
}
}
void runLedAnimation() {
for (int i = 0; i < NUM_LEDS; i++) {
leds[i] = CHSV(animHue + (i * 8), 255, headlightsOn ? 255 : 40);
}
animHue++;
FastLED.show();
}
// ═══════════════════════════════════════════
// user_setup() — CALLED ONCE
// ═══════════════════════════════════════════
void user_setup() {
// Motors
pinMode(PIN_MOTOR_A_PWM, OUTPUT);
pinMode(PIN_MOTOR_A_DIR1, OUTPUT);
pinMode(PIN_MOTOR_A_DIR2, OUTPUT);
pinMode(PIN_MOTOR_B_PWM, OUTPUT);
pinMode(PIN_MOTOR_B_DIR1, OUTPUT);
pinMode(PIN_MOTOR_B_DIR2, OUTPUT);
// Headlights & Horn
pinMode(PIN_HEADLIGHTS, OUTPUT);
pinMode(PIN_HORN, OUTPUT);
// LED strip
FastLED.addLeds<WS2812, PIN_LED_STRIP, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(120);
// Servo
cameraServo.attach(14);
cameraServo.write(90);
}
// ═══════════════════════════════════════════
// user_loop() — RUNS CONTINUOUSLY
// ═══════════════════════════════════════════
void user_loop() {
// LED animations (always running)
runLedAnimation();
// Periodic status report every 5 minutes
if (millis() - lastStatusReport > 300000) {
lastStatusReport = millis();
// juvantia_send("Status: OK"); // Phase 2 API
}
}
// ═══════════════════════════════════════════
// deck() — COMMAND HANDLER FROM DECK CONTROL
// ═══════════════════════════════════════════
void deck(String type, String name, int value) {
if (type == "AXIS") {
if (name == "Drive") {
currentSpeed = value;
int multiplier = boostActive ? 2 : 1;
int pwm = map(value, -100, 100, -255, 255) * multiplier;
pwm = constrain(pwm, -255, 255);
// Differential steering: both motors for drive
setMotorA(pwm);
setMotorB(pwm);
}
if (name == "Steering") {
currentSteering = value;
// Differential: slow down one side
int base = map(currentSpeed, -100, 100, -255, 255);
int steerFactor = map(value, -100, 100, -128, 128);
setMotorA(constrain(base + steerFactor, -255, 255));
setMotorB(constrain(base - steerFactor, -255, 255));
}
if (name == "CameraPan") {
int angle = map(value, -100, 100, 0, 180);
cameraServo.write(angle);
}
}
if (type == "BUTTON") {
if (name == "Headlights") {
headlightsOn = (value == 1);
digitalWrite(PIN_HEADLIGHTS, headlightsOn ? HIGH : LOW);
// LED strip brightness also changes (see user_loop)
}
if (name == "Horn") {
digitalWrite(PIN_HORN, value ? HIGH : LOW);
}
if (name == "Boost") {
boostActive = (value == 1);
}
}
}4. Available Arduino Functions
You have full access to the Arduino framework and ESP-IDF functions. Here are the most commonly used:
Digital I/O
pinMode(pin, OUTPUT); // Set pin as output
pinMode(pin, INPUT); // Set pin as input
pinMode(pin, INPUT_PULLUP); // Set pin as input with internal pull-up resistor
digitalWrite(pin, HIGH); // Set pin to 3.3V
digitalWrite(pin, LOW); // Set pin to 0V (ground)
int val = digitalRead(pin); // Read pin state: HIGH or LOWAnalog / PWM
int val = analogRead(pin); // Read analog voltage: 0–4095 (12-bit)
// ⚠ Use GPIO 1-10 (ADC1) — ADC2 unreliable with Wi-Fi
analogWrite(pin, duty); // PWM output: 0–255
// Works on ANY GPIO
// Advanced PWM (higher resolution):
ledcAttach(pin, freq, resolution); // e.g., ledcAttach(5, 5000, 8)
ledcWrite(pin, duty); // duty: 0 to (2^resolution - 1)Timing
unsigned long now = millis(); // Milliseconds since boot (unsigned long)
unsigned long us = micros(); // Microseconds since boot
delayMicroseconds(10); // OK for very short pauses (< 100µs)
// ❌ delay(ms) — NEVER USE IN user_loop() or deck()Math
int mapped = map(value, fromLow, fromHigh, toLow, toHigh);
// Example: map(75, -100, 100, 0, 255) → 221
int limited = constrain(value, min, max);
// Example: constrain(300, 0, 255) → 255
int absolute = abs(value);
// Example: abs(-75) → 75
float s = sin(radians(angle));
float c = cos(radians(angle));
int r = random(min, max);
// Example: random(0, 100) → random number 0-99Serial (Debug Output)
Serial.begin(115200); // In user_setup()
Serial.println("Hello!"); // Print to serial monitor
Serial.printf("Speed: %d\n", speed); // Formatted output💡 Serial output is useful for debugging via USB. It will appear in PlatformIO Serial Monitor.
String Operations
String msg = "Hello " + String(42); // "Hello 42"
int len = msg.length(); // 8
bool eq = (name == "Drive"); // String comparison
char c = msg.charAt(0); // 'H'
int idx = msg.indexOf("lo"); // 3
String sub = msg.substring(0, 5); // "Hello"5. Using Arduino Libraries
How to Add Libraries
In Step 3 of the Fabrica UI, there is a "Libraries" section above the code editor. You can:
- Click popular library buttons (FastLED, Servo, etc.) — one click adds them
- Type a custom library name in the format
owner/library(PlatformIO registry format) - Libraries are added to
platformio.iniaslib_depsat compile time
Popular Libraries for Robotics
FastLED — Addressable LED Control
Library: fastled/FastLED#include <FastLED.h>
#define PIN_LEDS 48
#define NUM_LEDS 60
CRGB leds[NUM_LEDS];
void user_setup() {
FastLED.addLeds<WS2812, PIN_LEDS, GRB>(leds, NUM_LEDS);
FastLED.setBrightness(100);
}
void user_loop() {
// Solid color
fill_solid(leds, NUM_LEDS, CRGB::Red);
// Rainbow
fill_rainbow(leds, NUM_LEDS, millis() / 10, 7);
// Individual LED
leds[0] = CRGB(255, 0, 128);
FastLED.show();
}ESP32Servo — Servo Motors
Library: madhephaestus/ESP32Servo#include <ESP32Servo.h>
Servo myServo;
void user_setup() {
myServo.attach(9); // GPIO 9
myServo.write(90); // Center position (0-180)
}
void deck(String type, String name, int value) {
if (name == "CameraPan") {
int angle = map(value, -100, 100, 0, 180);
myServo.write(angle);
}
}Adafruit NeoPixel — Alternative LED Library
Library: adafruit/Adafruit NeoPixel#include <Adafruit_NeoPixel.h>
#define PIN_LEDS 48
#define NUM_LEDS 30
Adafruit_NeoPixel strip(NUM_LEDS, PIN_LEDS, NEO_GRB + NEO_KHZ800);
void user_setup() {
strip.begin();
strip.setBrightness(100);
strip.show();
}
void user_loop() {
// Theater chase
static int pos = 0;
strip.clear();
for (int i = pos; i < NUM_LEDS; i += 3) {
strip.setPixelColor(i, strip.Color(0, 150, 255));
}
strip.show();
pos = (pos + 1) % 3;
}ArduinoJson — JSON Parsing
Library: bblanchon/ArduinoJson#include <ArduinoJson.h>
void processSensorData() {
StaticJsonDocument<200> doc;
doc["temp"] = 23.5;
doc["humidity"] = 67;
doc["battery"] = 85;
String output;
serializeJson(doc, output);
// output: {"temp":23.5,"humidity":67,"battery":85}
}PID Controller
Library: br3ttb/PID#include <PID_v1.h>
double setpoint = 0; // target speed
double input = 0; // current speed (from encoder)
double output = 0; // PWM output
PID speedPID(&input, &output, &setpoint, 2.0, 0.5, 0.1, DIRECT);
void user_setup() {
speedPID.SetMode(AUTOMATIC);
speedPID.SetOutputLimits(-255, 255);
}
void deck(String type, String name, int value) {
if (name == "Drive") {
setpoint = map(value, -100, 100, -1000, 1000); // target RPM
}
}
void user_loop() {
input = readEncoder(); // your encoder reading function
speedPID.Compute();
setMotor(output);
}6. System-Provided API (Phase 2)
These functions will be available in future releases. They are called from your code and communicate with Deck Control via the system WebSocket on Core 0:
| Function | Description | Status |
|---|---|---|
juvantia_send(String msg) | Send a text message to the operator's Deck Control | 🔜 Planned |
juvantia_log(String msg) | Write to the cloud-visible log in Fabrica | 🔜 Planned |
juvantia_sensor(String key, float val) | Push a sensor reading to the Deck Control dashboard | 🔜 Planned |
juvantia_alert(String msg) | Send an urgent notification to operator | 🔜 Planned |
Future example:
void user_loop() {
// Report battery every 10 minutes
if (millis() - lastBatteryReport > 600000) {
lastBatteryReport = millis();
float voltage = analogRead(PIN_BATTERY) * (3.3 / 4095.0) * 2.0;
juvantia_sensor("battery_v", voltage);
if (voltage < 3.3) {
juvantia_alert("Low battery! " + String(voltage) + "V");
}
}
}7. Common Patterns & Recipes
Pattern: Smooth Motor Control with Ramping
Don't jump from 0 to 255 instantly — it strains the motor and gears:
int currentPWM = 0;
int targetPWM = 0;
void deck(String type, String name, int value) {
if (name == "Drive") {
targetPWM = map(value, -100, 100, -255, 255);
}
}
void user_loop() {
// Smoothly ramp toward target (acceleration/deceleration)
if (currentPWM < targetPWM) {
currentPWM = min(currentPWM + 5, targetPWM);
} else if (currentPWM > targetPWM) {
currentPWM = max(currentPWM - 5, targetPWM);
}
setMotor(currentPWM);
}Pattern: Emergency Stop
Cut all motors if signal lost:
unsigned long lastCommandTime = 0;
#define SIGNAL_TIMEOUT 3000 // 3 seconds
void deck(String type, String name, int value) {
lastCommandTime = millis(); // Reset watchdog on any command
// ... handle commands normally
}
void user_loop() {
if (millis() - lastCommandTime > SIGNAL_TIMEOUT) {
// No commands for 3 seconds — emergency stop
setMotorA(0);
setMotorB(0);
// Optionally: flash LEDs as warning
}
}Pattern: Blinking LED Without delay()
unsigned long lastBlink = 0;
bool blinkState = false;
void user_loop() {
if (millis() - lastBlink > 500) { // 500ms = 1Hz blink
lastBlink = millis();
blinkState = !blinkState;
digitalWrite(PIN_STATUS_LED, blinkState);
}
}Pattern: Button Triggers Animation
bool alarmActive = false;
void deck(String type, String name, int value) {
if (name == "Alarm") {
alarmActive = (value == 1);
}
}
void user_loop() {
if (alarmActive) {
// Red-blue police flash
bool phase = (millis() / 200) % 2;
for (int i = 0; i < NUM_LEDS / 2; i++) {
leds[i] = phase ? CRGB::Red : CRGB::Blue;
}
for (int i = NUM_LEDS / 2; i < NUM_LEDS; i++) {
leds[i] = phase ? CRGB::Blue : CRGB::Red;
}
FastLED.show();
}
}Pattern: Differential Steering (Tank Drive)
int driveValue = 0;
int steerValue = 0;
void deck(String type, String name, int value) {
if (type == "AXIS") {
if (name == "Drive") driveValue = value;
if (name == "Steering") steerValue = value;
// Recalculate motors whenever either axis changes
int left = constrain(driveValue + steerValue, -100, 100);
int right = constrain(driveValue - steerValue, -100, 100);
setMotorA(map(left, -100, 100, -255, 255));
setMotorB(map(right, -100, 100, -255, 255));
}
}Pattern: Compass / Heading Hold
#include <Wire.h>
// ... compass library setup
float targetHeading = 0;
bool headingHoldActive = false;
void deck(String type, String name, int value) {
if (name == "HeadingHold") {
headingHoldActive = (value == 1);
if (headingHoldActive) {
targetHeading = readCompass(); // Lock current heading
}
}
if (name == "Steering" && !headingHoldActive) {
// Manual steering only when heading hold is off
steer(value);
}
}
void user_loop() {
if (headingHoldActive) {
float current = readCompass();
float error = targetHeading - current;
// Apply PID correction
int correction = computePID(error);
steer(correction);
}
}8. Compilation & Flashing
What Happens When You Press "Compile Firmware"
- Your code from the editor is sent to the Fabrica backend
- The backend generates a complete PlatformIO project:
project/ ├── platformio.ini ← board, libs ├── src/ │ ├── main.cpp ← System code (Core 0) + your code (Core 1) │ └── user_code.h ← Your code injected here └── lib/ └── (auto-downloaded) platformio runcompiles it into a.binfirmware file- The
.binis returned to your browser
Flashing to the Board
After compilation, the next step (Step 4) uses the Web Serial API to flash the .bin directly from your browser to the ESP32-S3:
- Connect the board via USB-C (the UART port, not OTG)
- Click "Flash Firmware" in Fabrica
- Browser requests serial port access (you grant permission)
- Firmware is written to flash memory
- Board reboots and starts running your code
9. Debugging
Serial Monitor
Add Serial.println() statements to debug your code:
void deck(String type, String name, int value) {
Serial.printf("[DECK] %s | %s | %d\n", type.c_str(), name.c_str(), value);
// ... rest of your logic
}Common Issues
| Symptom | Likely Cause | Fix |
|---|---|---|
| Robot doesn't respond | No Wi-Fi connection | Check SSID/password in config |
| Motors don't move | Wrong pin or no pinMode(OUTPUT) | Verify pin number + user_setup() |
| Jerky movement | Using delay() | Replace with millis() |
| LEDs flicker | Not calling FastLED.show() | Add to user_loop() |
| Servo shakes | Power from ESP32 3.3V | Use external 5V BEC |
| Analog reads are wrong | Using ADC2 with Wi-Fi | Use ADC1 (GPIO 1–10) |
| Board doesn't boot | External load on strapping pin | Move wire to non-strapping GPIO |
deck() not called | Command name mismatch | Check exact spelling from Step 2 |
10. Full Glossary
| Term | Definition |
|---|---|
user_setup() | Function called once on Core 1 startup. Initialize hardware here. |
user_loop() | Function called continuously on Core 1. Put timed logic and animations here. |
deck() | Function called when Deck Control sends a command. Receives type, name, and value. |
type | "AXIS" (analog, -100 to +100) or "BUTTON" (digital, 0 or 1) |
name | The exact string name of the control as defined in Step 2 (e.g., "Drive") |
value | Axis: -100 to +100. Button: 0 (OFF) or 1 (ON) |
millis() | Returns milliseconds since boot. Use for all timing. |
map(v, a, b, c, d) | Linearly maps value v from range [a,b] to range [c,d] |
constrain(v, min, max) | Clamps value v between min and max |
analogWrite(pin, duty) | PWM output, duty 0–255 |
analogRead(pin) | Read analog voltage, returns 0–4095 (12-bit) |
pinMode(pin, mode) | Configure pin: INPUT, OUTPUT, or INPUT_PULLUP |
digitalWrite(pin, val) | Set digital output: HIGH (3.3V) or LOW (0V) |
digitalRead(pin) | Read digital input: HIGH or LOW |
delay() | ❌ FORBIDDEN — blocks the CPU core |
FastLED.show() | Push LED data to the strip (FastLED library) |
servo.write(angle) | Move servo to angle 0–180 (Servo library) |
PlatformIO | Build system used to compile the firmware |
lib_deps | Libraries listed in platformio.ini — auto-downloaded |
| Core 0 | System CPU core — Wi-Fi, WebSocket, out of your control |
| Core 1 | User CPU core — your code runs here |
| Deck Control | Web control interface at deck.juvantia.org — joysticks, buttons, camera, telemetry |
| Fabrica | Web portal where you configure, code, compile, and flash |