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:
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:
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:
<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:
#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):
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.
#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.
| 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.
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.
Returns angle (servo), steps (stepper), or counts (bus servo); negative on failure.
#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:
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.
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:
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:
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:
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:
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
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):
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 throttledloop(), 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 itstarget. The livepositionfeedback 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.