The sandbox
Read this before you build. Every limit here is deliberate, and none of them are configurable.
No network. At all.
A Content Security Policy is injected before your code runs:
default-src 'self' 'unsafe-inline' data: blob:; connect-src 'none';
script-src 'self' 'unsafe-inline'
connect-src 'none' means no fetch, no XHR, no WebSocket, no beacon. No
CDN scripts, no Google Fonts, no analytics, no error reporting, no API you own.
Everything ships in your folder. Inline your CSS, embed images as data URIs, bundle your own fonts.
This is the single most important thing to design around, and the most common reason a plugin that worked in a browser does nothing on the device. The simulator does not enforce it. Test on a device before you publish.
The only outside information available to you is data.weather().
You cannot leave your folder
File access is scoped to your own plugin directory. You cannot read another plugin's files, the app's files, or anything else on the device.
Navigation away from your own bundle is refused: links, redirects,
window.location. Your plugin is one page, for its whole life.
localStorage does not persist, hairobo.storage does
The WebView's own storage is non-persistent: localStorage, cookies and
caches are cleared between runs, and always will be. Anything you keep there
is gone the moment your plugin closes.
Use hairobo.storage instead. It survives between runs, is scoped to your
plugin id, needs no permission, and is deleted when the user uninstalls you.
Full API in api.md. Its caps:
| keys | 200 |
| key length | 128 characters |
| one value | 16 KB |
| whole store | 256 KB |
Values are strings, so JSON.stringify your own objects.
⚠️ Hitting a cap REJECTS the promise; it does not resolve false. That is
the opposite of the rate limits below, and deliberately so: a rate limit clears
on its own and retrying is correct, whereas a full store stays full until you
remove something. Catch it.
It is not a database. If you need more than 256 KB, this is not the API for what you are building.
Rate limits
voice.speak |
one per 0.4s, 300 characters |
motor.* |
one move per 0.35s |
| camera events | about 6 per second, up to 8 faces |
Exceeded calls resolve false and are dropped, not queued. A backlog of
stale commands is worse than a missed one, especially for movement.
Ambient plugins yield
An ambient plugin gives up the screen the moment the robot leaves idle, and
whenever the display deep-dims for battery. Do not depend on running
continuously.
What you can rely on
Your page is a full modern WebView. Canvas, WebGL, CSS animation, Web Audio, touch: all of it works. The limits above are about the outside world, not about what you can build on screen.
The simulator fakes the camera
The simulator has no camera. It emits synthetic face events at the same 6 Hz
the device uses, with however many people you set in the sidebar, spread across
the frame and drifting. That is enough to exercise counting and layout, and
nothing like a real room. Test multi-face on a device before you publish.
Storage in the simulator is backed by the browser's own localStorage with the
same caps enforced, so you hit a cap there before a user hits it for you.
How the simulator runs your plugin
Drop your folder on it. It reads the files straight off your machine: nothing is uploaded, nothing is downloaded, and you do not need a server. This is the way to run your own plugin.
It provides the SDK, exactly as the app does. hairobo is defined before
your first script runs, so your plugin needs no script tag for it. The same
code runs here and on a robot, with nothing in it that exists only for the
simulator.
A reference that is not in your folder is named in the log. A missing
stylesheet used to show up as a page that simply looked wrong; now it says which
file it wanted. The entry comes from your manifest's entry, so a plugin that
does not use index.html still runs.
The path box is for the bundled examples, which sit next to the simulator on its own server. Your own plugin will not be there. Use the folder.