Plugins
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:
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
| Method | What 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:
- page reload (via localStorage autosave, 400 ms debounce)
- session EXPORT to JSON file
- session IMPORT from JSON file (plugins are force-remounted so
onInitre-reads viaapi.recall())
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
- see other plugins' iframes
- read the host's
localStorage - access
fetch,XMLHttpRequest, or any network primitive - modify other device cards
- override built-in PinScope behavior
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.
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:
- Stateful UI: keep state on
thisand re-render inonStateby reading the lateststateargument. - Persistent settings: read with
api.recallinonInit, write withapi.persiston user input. - Event handlers: re-attach every render. The sandbox's DOM is small enough that re-binding is cheaper than tracking node identity.
- HTML escaping:
api.renderis raw HTML by design. Escape user-typed strings before interpolating them.
Plugin development workflow
- Write the plugin in a
.jsfile in your editor. - Open
pinscope.htmlin the browser. - Press
P, click FROM FILE, pick your file. The plugin loads with the file's stem as its id. - Connect a device. Your plugin's panel appears at the bottom of the device card.
- 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).