Extensions overview

Extensions add support for hardware devices — servos, steppers, LED strips, sensors and cameras. Register them in JS, include them in the sketch.

Using extensions in JavaScript

Register extensions before connecting, and use them by name:

JS
sketch.js — register and use extensions
const arduino = new Arduino();

arduino.add('myServo', new Servo());
arduino.add('strip',   new NeoPixel());
arduino.add('sonar',   new Ultrasonic());

arduino.connect('192.168.1.42');

arduino.on('ready', () => {
    arduino.myServo.attach(9);
    arduino.strip.init(6, 30);
    arduino.sonar.attach(7, 8);
});

Each extension automatically gets a logical id — its slot number on the board, counted per type from 0 (the first servo is servo 0 and the first stepper is stepper 0, whatever else the page adds). Multiple instances of the same type are supported, up to the board's slot count for that type:

Type Instances at once
Servo 8
Stepper 6
Bus servo 16
NeoPixel, Ultrasonic, Encoder 4 each
IMU 2

Adding one more than the board holds prints a warning — the board would ignore it.

remove()

arduino.remove(name) is the inverse of add(): it releases the device on the board (detaches it, freeing its pin, bus ID or PWM channel), drops it from any groups, and frees its logical id for the next add() to reuse. Use it when a page changes what hardware it drives while running:

JS
sketch.js — swap a servo for a stepper
arduino.remove('arm');                    // detach the servo on the board
arduino.add('arm', new Stepper());        // a new stepper takes its place
arduino.arm.attach(2, 3);

add() with a name that's already in use does the remove() for you, so the two lines above can be just the add(). The removed object is left inert — calling anything on it warns and sends nothing, because its old id may now belong to another device. Objects the sketch created (see below) belong to the sketch and can't be removed from the browser.

Script loading order

pardalote.js is the all-in-one bundle — core plus every device extension — so a single script tag is all you need, loaded before your sketch:

index.html — script loading order
<script src="pardalote.js"></script>
<script src="sketch.js"></script>

Every extension (Servo, Stepper, BusServo, NeoPixel, Ultrasonic, IMU, Encoder, Camera) is already inside the bundle — you just arduino.add(...) the ones you use. (Advanced: the modular sources in lib/src/ — pardalote-core.js plus one pardalote-<device>.js each — can be included individually instead; the bundle is exactly those concatenated.)

Enabling extensions in the firmware

Extensions are opt-in on the Arduino side too. Add the headers you need to your sketch:

Arduino
sketch.ino — opt-in extensions
#include <Pardalote.h>
#include <PardaloteServo.h>
#include <PardaloteNeoPixel.h>
// #include <PardaloteStepper.h>
// #include <PardaloteBusServo.h>
// #include <PardaloteUltrasonic.h>
// #include <PardaloteIMU.h>
// #define CAMERA_MODEL_XIAO_ESP32S3
// #include <PardaloteCamera.h>

void setup() { Pardalote.begin(); }
void loop()  { Pardalote.run();   }

Each extension self-registers when included — no other changes required. Only the extensions you #include get compiled into the binary.

Creating extension objects in the firmware

Extension objects don't have to be created in the browser with arduino.add() — the sketch can create them too, and every browser receives them automatically. PardaloteServo.attach("pan", 9) on the Arduino makes arduino.pan appear in every connected browser as a full Servo instance, identical to one created with add() — present before 'ready' fires, and pushed to browsers that connect later.

Every multi-instance device supports this — Servo, Stepper, Bus servo, NeoPixel, Ultrasonic and IMU (only the singleton Camera is browser-only):

PardaloteServo.attach(name, pin, [minPulse], [maxPulse])
PardaloteStepper.attach(name, step, dir, [en]) · attach4wire(name, p1, p2, p3, p4)
PardaloteBusServo.attach(name, servoId, [series])
PardaloteNeoPixel.attach(name, pin, count, [type])
PardaloteUltrasonic.attach(name, trig, [echo])
PardaloteIMU.attach(name, [model], [addr], [sda], [scl])

name is the browser-side handle (arduino.<name>, max 15 chars). Each attach returns the logical id used by the sketch-side calls below, or −1 if no slot is free.

Arduino
sketch.ino — the sketch creates a servo
#include <Pardalote.h>
#include <PardaloteServo.h>

int pan;

void setup() {
    Pardalote.begin();
    pan = PardaloteServo.attach("pan", 9);   // arduino.pan now exists in every browser
    PardaloteServo.write(pan, 90);
}

Creating an extension object is sharing it — unlike raw pins there's no separate share() step, because a Pardalote extension has no life outside Pardalote. arduino.on('share', ({ name, extension }) => …) fires the moment it appears; calling attach again with the same name reuses the same object; names that would collide with the browser core API (like "connect") are refused with a console warning. A device that should stay private to the sketch shouldn't go through Pardalote at all — drive it with the plain library directly (a raw Servo on its own pin, a raw SCServo bus on its own UART), the way unshared pins just use pinMode().

Reading and writing actuators from the sketch

share/send cover raw pins. For the extension actuators — browser-created or sketch-created — each type gives the sketch a small bus object (PardaloteServo, PardaloteStepper, PardaloteBusServo). The browser and the sketch share the same actuator (last writer wins), just as they share raw pins. Everything is addressed by logical id — what attach returns, and the same id the browser and groups use.

Object scan() returns read(id) returns id is
PardaloteServo attached servo ids angle (0–180) logical id (the browser's per-type slot, or returned by sketch attach)
PardaloteStepper attached stepper ids position (steps) logical id
PardaloteBusServo responding hardware ids on the bus (discovery) position (counts) logical id (returned by attach)

scan()

Lists what's there.

PardaloteType.scan(buffer, maxCount)
Parameter Type Description
buffer int[] / uint8_t[] Array to fill with ids.
maxCount int Size of the array.

Returns the number of ids written. For PardaloteBusServo, scan() pings the bus and reports the hardware ids that respond — that's discovery; attach() the ones you want to drive, then address them by the logical id it returns.

Arduino
int ids[8];
int n = PardaloteServo.scan(ids, 8);
for (int i = 0; i < n; i++) Serial.println(PardaloteServo.read(ids[i]));

read()

Reads one actuator's position by logical id.

PardaloteType.read(id)

Returns angle (servo), steps (stepper), or counts (bus servo); negative on failure.

Arduino
#include <Pardalote.h>
#include <PardaloteBusServo.h>

int wrist;

void setup() {
    Pardalote.begin();
    wrist = PardaloteBusServo.attach("wrist", 1);   // adopt bus id 1 → logical id
}

void loop() {
    Pardalote.run();
    int pos = PardaloteBusServo.read(wrist);        // logical id → position (counts)
    if (pos >= 0) digitalWrite(LED_BUILTIN, pos > 2048 ? HIGH : LOW);
}

write() and friends

The same objects command the actuators, all by logical id:

Arduino
PardaloteServo.write(id, 90);            // angle 0–180
PardaloteServo.writeTimed(id, 90, 1000); // over 1 s (board-interpolated)
PardaloteServo.stop(id);

PardaloteStepper.moveTo(id, 2000);       // steps
PardaloteStepper.move(id, -400);
PardaloteStepper.stop(id);

PardaloteBusServo.write(id, 2048);       // counts (optional speed, acc)
PardaloteBusServo.torque(id, false);     // release / hold

All three run through the same command path the browser uses — so they respect soft limits, cancel timed moves, and auto-echo the commanded value back to the browser so its record stays in sync.

Board-authored gestures

"Whoever speaks is in control": the browser can compose-and-play a gesture, and so can the sketch. The board authors a gesture as a PardaloteSeg[] — { curve, dur, value } in the actuator's native unit — and plays the schedule on its own clock, no browser needed. Because Pardalote is 32-bit only, a static const PardaloteSeg[] lives in flash and costs no RAM.

PardaloteServo.gesture(id, segments, count, [flags])
Arduino
static const PardaloteSeg NOD[] = {
    { CURVE_EASE_OUT,    250,  60 },   // to 60°  (absolute by default)
    { CURVE_EASE_IN_OUT, 400, 120 },
};
PardaloteServo.gesture(pan,  NOD,  2);     // servo degrees
PardaloteStepper.gesture(lift, LIFT, 3);   // stepper steps
PardaloteBusServo.gesture(grip, GRIP, 2);  // bus-servo counts

Segments are absolute by default (value is a target); pass flags = 0 for relative (value is a delta off the live position). Curves are the CURVE_* ids (CURVE_LINEAR, CURVE_EASE_IN, CURVE_EASE_OUT, CURVE_EASE_IN_OUT, CURVE_BACK). This is the sketch twin of JavaScript servo.gesture() — same segment model, same on-board player, so a board- and a browser-authored gesture play identically.

Coordinate several actuators with Pardalote.gesture() — the board-side twin of arduino.gesture(). Add one lane per actuator by DEVICE_* id; shorter lanes are padded so every lane arrives together, and all start on one clock:

Arduino
Pardalote.gesture()                                  // each array a PardaloteSeg[] as above
    .add(DEVICE_SERVO,   shoulder, SHOULDER, 2)
    .add(DEVICE_SERVO,   wrist,    WRIST,    1)      // shorter → padded to arrive together
    .add(DEVICE_STEPPER, lift,     LIFT,     3)
    .play();

Reshape a gesture — the board-side twin of the JS scale / speed / crop. Pass a PardaloteGestureMod to a single actuator's gesture(), or chain .scale() / .speed() / .crop() on the coordinated builder. The transform runs once, at gesture start, over the RAM copy each actuator already keeps — no extra buffer — and produces the byte-identical result to the browser doing the same:

Arduino
PardaloteServo.gesture(pan, NOD, 3, GESTURE_FLAG_ABSOLUTE, { 0.7f, 0.5f, 0.2f, 0.8f });  // {scale,speed,crop}

Pardalote.gesture()
    .add(DEVICE_SERVO, shoulder, SHOULDER, 2)
    .add(DEVICE_SERVO, wrist,    WRIST,    1, true, 1.3f)   // per-lane amplitude (laneScale)
    .speed(1.5f).crop(0.0f, 0.75f)                          // group-wide: keeps lanes phase-locked
    .play();

speed and crop are group-wide (one factor, one window on the shared timeline) so the lanes stay in step; scale may differ per lane. scale multiplies a relative delta directly and scales an absolute target around the lane's starting target (origin + (to − origin) × k); a cropped relative gesture ends off-home. It's applied board-side, so the on-wire frame is unchanged.

Coordinated write / writeTimed — the twins of arduino.write() / arduino.writeTimed() — take one target per actuator instead of a schedule:

Arduino
Pardalote.write().add(DEVICE_SERVO, pan, 90).add(DEVICE_STEPPER, lift, 0).play();            // immediate
Pardalote.writeTimed(1500).add(DEVICE_SERVO, pan, 45).add(DEVICE_SERVO, tilt, 135).play();   // arrive together

Chain gestures headlessly with onGestureDone(id, cb) — the board-side whenDone(). The callback fires when the last segment lands, so the sketch can run a sequence (or an idle loop) with no browser at all:

Arduino
void onNodDone(int id) { PardaloteServo.gesture(id, IDLE, 2); }   // loop back to idle
PardaloteServo.onGestureDone(pan, onNodDone);

Watchers stay in sync: a board-authored gesture broadcasts its existence (never its shape) to every browser, which sees the actuator's isGesturing flag flip and gesturestart / gestureend fire — the same signals a browser-authored gesture gives. See the board-gestures-PWM-servos and board-gestures-bus-servos sketches in the Arduino examples for a complete headless creature-head (both step through the scale/speed/crop shaping cycle on a button), and the Gesture page for the concept.

Status helpers

Arduino
PardaloteServo.isMoving(id);          // timed move in progress
PardaloteStepper.distanceToGo(id);
PardaloteStepper.isRunning(id);
PardaloteBusServo.feedback(id);       // position, load, voltage, temperature in one read
PardaloteBusServo.isMoving(id);       // the servo's own Moving flag
PardaloteBusServo.arrived(id);

For bus servos, isMoving(id) / arrived(id) read the servo's own Moving flag — its honest "am I still moving?", accounting for deadband and settling. It's one bus read (the servo can't notify you — you ask when you want to know):

Arduino
Example — wait for a bus servo to arrive
PardaloteBusServo.write(wrist, 3000);
while (PardaloteBusServo.isMoving(wrist)) { /* do other work */ }
// arrived — trigger the next thing
  • A bus servo read/scan/write is a blocking bus transaction — fine in setup() or a throttled loop(), not a tight high-rate loop competing with the browser's own polling.
  • Sketch writes update the browser's record automatically. A sketch write echoes the commanded value to the browser exactly as if the browser had issued it — a PWM servo sets the browser's angle, a stepper or bus servo sets its target. The live position feedback is separate — that still comes from polling.

The extensions

Extension Supports Limit
Servo PWM hobby servos 8
Stepper STEP/DIR and 4-wire drivers via AccelStepper 6
Bus servo Feetech ST/SMS and SC/SCS serial servos 16
NeoPixel WS2812B-style LED strips 4 strips
Ultrasonic HC-SR04 and similar distance sensors 4
IMU InvenSense MPU and STMicro LSM6 families 2
Camera ESP32 camera modules (MJPEG over HTTP) 1 stream

Groups let you drive several actuators together — one message, coordinated arrival, and coordinated gestures.

Rolling your own

An extension is a JS file paired with an Arduino header: the JS side sends commands over the shared protocol, the Arduino side registers handlers for them. The built-in extensions are the best templates — pick the one closest to your hardware and start from its source. See Protocol for the frame format.