PINSCOPEField Instrument 005
GitHub

Plugins

Sandboxed extensions for device cards

PinScope supports user-loaded extensions. Each plugin gets a panel on every device card and runs inside a sandboxed iframe with no network, no DOM access outside its own body, no localStorage of its own. Communication with the host happens through postMessage over a per-instance bridge id.

Loading a plugin

Open the manager with the PLUGINS button on the rail, or press P. Paste plugin source or load it from a file, give it a name, click LOAD.

Loaded plugins persist in localStorage under the key pinscope.plugins.v1 and re-mount automatically the next time the page opens. The checkbox next to each loaded plugin toggles enable; REMOVE deletes it entirely.

API contract

Inside the sandbox iframe, plugins call PinScope.register({...}) exactly once with these fields:

PLUGIN SHAPE
PinScope.register({
  id:       'my-plugin',      // unique alphanumeric identifier
  name:     'My Plugin',      // human label shown in the panel head
  version:  '1.0',            // any string
  onInit(api)         { /* called once after registration */ },
  onState(state, api) { /* called for every state packet  */ },
  onDestroy()         { /* called on disconnect or disable */ },
});

The state argument matches the wire protocol packet: { d: [14], a: [6], v: [4], f: [14], m: [14], t: timestamp }.

The api object

MethodWhat it does
api.send(obj)send a wire-protocol command (subject to the same validation user actions go through)
api.getState()returns a Promise resolving to a state snapshot
api.log(msg)prints to the host's developer console
api.render(html)replaces the plugin panel body with HTML (script tags stripped, defense in depth)
api.renderSVG(svgText)same but without script stripping, for trusted SVG content
api.persist(key, value)save plugin state to the session; survives reload and export
api.recall(key)returns a Promise resolving to a previously persisted value

Persistence round-trip

Anything a plugin writes through api.persist lands in device.pluginState[plugin.id][key] on the host and is part of the session snapshot. Plugin state survives:

Gotcha

Always read persisted state in onInit, never cache it from a previous lifecycle. Plugins are torn down and remounted on session import.

Example plugins

Four plugins ship with the repo under plugins/. They progress from "minimal" to "full API workout."

hello.js (28 lines)

The simplest possible plugin. Prints A0 with an ASCII bar. Useful as a template for new plugins.

PinScope.register({
  id: 'hello',
  name: 'Hello World',
  version: '1.0',

  onInit(api) {
    api.render('<p>waiting for first state packet...</p>');
  },

  onState(state, api) {
    const a0 = state.a[0];
    const bars = '█'.repeat(Math.floor(a0 / 100));
    api.render(
      '<h3>A0 LIVE</h3>' +
      '<p>' + a0 + ' / 1023</p>' +
      '<p style="color:#f5b724">' + bars + '</p>'
    );
  },
});

gauge.js (73 lines)

An SVG arc gauge for any analog pin. Demonstrates renderSVG (which bypasses the script stripper for trusted SVG content), persist / recall for the selected pin, and a button group rendered into the plugin's own DOM.

servo-sweep.js (71 lines)

Drives a PWM pin in a slow triangle wave. Demonstrates api.send to issue wire-protocol commands and a small UI with selectable PWM pin and start/stop controls.

field-notes.js (145 lines)

A persistent notebook attached to the device card. Free-form notes textarea plus a list of timestamped "moments" that snapshot the current state with optional labels. Demonstrates the full persist / recall round-trip for both simple (string) and complex (array of objects) value types.

The recommended starting point for "useful" plugins, since notebooks are something most users want and the plugin doubles as a worked example of the API.

Security model

The iframe sandbox is sandbox="allow-scripts" only. The lack of allow-same-origin is deliberate and blocks the iframe from reaching the parent document, navigating the top frame, or making same-origin network requests. The bridge id is randomized per instance, so a malicious plugin can't impersonate another plugin's messages.

User-supplied plugin source is embedded into the iframe via JSON.stringify and run with eval inside the sandbox, which prevents template-literal breakout attacks on the host. The closing script> tag in the srcdoc is split across two string concatenations so the host HTML parser doesn't terminate the outer script block early.

What plugins cannot do, by design

Wire commands route through the same device.send() path as user actions, so any abuse appears in the Wire Log and can be paused or audited.

Caveat

Plugin source is still user-trusted code. PinScope sandboxes plugins against affecting the host, but a plugin you load can issue any wire command the firmware accepts. Treat plugin source like any other code you run on your hardware.

Writing your own

Start from plugins/hello.js. Replace the onState body with whatever you want to show. The sandbox iframe's body is yours to draw into. The styles inside the iframe inherit a sensible default (amber on dark, monospace, small padding) but you can override them with your own <style> blocks.

A few patterns that work well:

Plugin development workflow

  1. Write the plugin in a .js file in your editor.
  2. Open pinscope.html in the browser.
  3. Press P, click FROM FILE, pick your file. The plugin loads with the file's stem as its id.
  4. Connect a device. Your plugin's panel appears at the bottom of the device card.
  5. Iterate: edit the file, hit REMOVE in the plugin manager, reload the file. Hot-reload is on the roadmap but not shipped yet.

Source is plain JS. Use whatever debugger you'd use for any sandboxed iframe (DevTools shows the iframe as a separate execution context).