The Arduino sketch

Writing Arduino code alongside the browser — sharing pins, sending values, and driving actuators from the sketch.

The model

The minimal sketch is Pardalote.begin() + Pardalote.run(), but you can also write Arduino code that reads sensors, drives pins, and runs a state machine — alongside the browser.

The Arduino is just another voice in a flat command structure. Both the Arduino sketch and the browser can read and write any pin using the standard Arduino / JS APIs. Whoever wrote last wins on the actual pin state. There's no pin reservation, no negotiation — your sketch and your JS code share the same hardware and you keep them coherent.

Pardalote.begin()

Starts Pardalote. Call once in setup(). Three forms:

Pardalote.begin()
Pardalote.begin(PARDALOTE_WIFI)
Pardalote.begin(PARDALOTE_SERIAL)
Form What it does
begin() The default: joins WiFi (see WiFi configuration) and starts the WebSocket server, and listens on the USB cable. If a browser connects over USB with a deliberate port-picker gesture, the board drops WiFi and switches to USB (see switching to USB). One-way — reboot to return to WiFi.
begin(PARDALOTE_WIFI) WiFi only. Same as begin() but does not listen on USB — the opt-out for "nobody grabs my board over the cable."
begin(PARDALOTE_SERIAL) Skips WiFi entirely and talks over the USB cable only — the browser connects with connectSerial().

The one exception is the UNO R4 Minima — it has no radio, so every begin() form starts the serial transport.

Only the first call counts. If begin() appears twice, for example after combining two example sketches, the extra call is ignored and the Serial Monitor shows [Pardalote] begin() was called more than once — ignoring the extra call. Running the startup twice would restart WiFi and destabilise the connection.

In serial mode, Serial.print from your sketch still works — the output travels between protocol messages and appears in the browser as the 'log' event (and in the Serial Monitor as usual when the browser isn't connected). Don't Serial.write raw binary; text is fine.

Pardalote.requireKey()

Optional. Call before begin() to require a connection key — only browsers that pass the same key to connect(ip, { key }) or connectSerial({ key }) get in. It works over both transports and composes with every begin() form.

Pardalote.requireKey(key)
Arduino
void setup() {
  Pardalote.requireKey("robot-arm-3");   // optional — before begin()
  Pardalote.begin();                     // or begin(PARDALOTE_WIFI) / begin(PARDALOTE_SERIAL)
}

Over WiFi it's an accident-prevention latch against joining a neighbour's board on a shared network. Over USB — where the cable already picks the board — the key becomes a board-identity check: a student who grabbed the wrong physical board gets a clear "wrong key for this board" error instead of silently driving it. Not security — the key travels unencrypted (over the network, or in the clear on USB). Up to 32 characters.

Pardalote.setTxPower()

Optional, ESP32 only. Sets the WiFi transmit power. Call it before begin() so the board also joins the network at that power.

Pardalote.setTxPower(power)
Arduino
void setup() {
  Pardalote.setTxPower(WIFI_POWER_15dBm);   // optional — before begin()
  Pardalote.begin();
}

power is one of the ESP32 core's WIFI_POWER_* values, from WIFI_POWER_19_5dBm (full power) down to WIFI_POWER_2dBm.

Seeed XIAO ESP32-S3 and ESP32-C3 boards default to WIFI_POWER_8_5dBm. At full power their antenna misbehaves: they join slowly or not at all, the connection lags and drops, and camera video stalls. Lowering the transmit power is the widely reported fix, and it made a large difference in our tests. The Serial Monitor shows the setting at boot (WiFi transmit power: 8.5 dBm). If a XIAO is far from the router and the signal is weak, try WIFI_POWER_15dBm or WIFI_POWER_17dBm. WIFI_POWER_19_5dBm restores full power.

Other ESP32 boards use full power unless you call it. See Troubleshooting.

Pardalote.run()

Services the connection: handles incoming commands, runs polls and timed moves. Call every pass of loop() — keep the loop non-blocking so it runs often.

Pardalote.run()

Pardalote.share()

Declares a pin's mode to the browser: "this pin exists, it's in this mode." Doesn't touch the hardware — you still call pinMode() yourself.

Pardalote.share(pin, mode, [interval], [threshold])
Parameter Type Description
pin int The pin to declare.
mode constant INPUT, OUTPUT, INPUT_PULLUP, INPUT_PULLDOWN, or Pardalote's ANALOG_INPUT_MODE.
interval int Optional. Registers a board-owned watch: values flow to every browser. For analog pins it's the browsers' update rate limit; digital changes transmit immediately.
threshold int Optional. Minimum change worth transmitting (0 = default: 1 for digital, the ADC noise floor for analog).

With an interval, the board owns the watch: values flow to every browser — including ones that connect later — with no JS call and no round trip, and only when the reading has changed by at least threshold. The watch survives browser disconnects.

Arduino
Pardalote.share(A0, ANALOG_INPUT_MODE, 50);      // browsers hear changes at most every 50 ms
Pardalote.share(A0, ANALOG_INPUT_MODE, 50, 8);   // …and only changes of 8+ counts

Without an interval (input modes), the browser auto-starts a default-interval (200 ms) poll for the pin — so it still receives values without declaring anything itself. For OUTPUT it's purely a declaration (no polling).

Pardalote.send()

Pushes a value to the browser. The browser caches it, fires arduino.pin(pin)'s 'change' listeners, and makes it available via arduino.digitalRead(pin) / analogRead(pin). Doesn't touch the hardware.

Pardalote.send(pin, value)
Parameter Type Description
pin int The pin the value belongs to.
value int The value to push.

Example — light switch

An LED controlled by two physical buttons and two browser buttons. Either side flips it; both stay in sync. The Arduino calls share once and send whenever its buttons fire:

Arduino
sketch.ino — light switch, Arduino side
#include <Pardalote.h>

const int LIGHT = 13;
const int BTN_ON = 4;
const int BTN_OFF = 5;

void setup() {
    Pardalote.begin();
    pinMode(LIGHT,  OUTPUT);
    pinMode(BTN_ON,  INPUT_PULLUP);
    pinMode(BTN_OFF, INPUT_PULLUP);
    Pardalote.share(LIGHT, OUTPUT);
}

void loop() {
    Pardalote.run();
    if (button_on_pressed) {
        digitalWrite(LIGHT, HIGH);
        Pardalote.send(LIGHT, HIGH);
    }
    // ... mirror for off ...
}

The browser writes the same pin and listens for changes, so a press on either side keeps both in sync:

JS
sketch.js — light switch, browser side
// Browser → Arduino: the on-screen buttons write the pin
onBtn.onclick  = () => arduino.digitalWrite(LIGHT, HIGH);
offBtn.onclick = () => arduino.digitalWrite(LIGHT, LOW);

// Arduino → browser: a change from either side lands here
arduino.pin(LIGHT).on('change', ({ value }) => {
    lightEl.textContent = value ? 'ON' : 'OFF';
});

Example — potentiometer

The Arduino announces an analog input; the browser receives values automatically with no JS-side setup:

Arduino
sketch.ino — announce an analog input
#include <Pardalote.h>

void setup() {
    Pardalote.begin();
    pinMode(A0, INPUT);
    Pardalote.share(A0, ANALOG_INPUT_MODE);   // browser auto-starts polling
}

void loop() {
    Pardalote.run();   // that's it — browser's polls are handled here
}
JS
sketch.js — receive with no setup
arduino.pin('A0').on('change', ({ value }) => updateDisplay(value));   // no pinMode, no analogRead

When not to share

Not every pin needs to be shared. In the light-switch example the two button pins are only used by the Arduino — the browser has its own buttons, so there's no reason to tell it about the physical ones. Share only the pins you want the browser to see.

Pardalote.send(pin, value) above is really one special case of a more general idea. The same send verb, given a string key instead of a pin number, becomes the message channel: send(key, value) pushes any named key/value to the browser — symmetric in both directions and carrying every basic type. Pushing a pin's value is just the pin-shaped form of it; use a string key for anything that isn't tied to a pin or a device.

See also: Messaging · Extensions overview · Servo · Stepper · Bus servo