Overview
Oriel OS runs JavaScript applications on an ESP32-S3 microcontroller with a round 466x466 pixel AMOLED display and capacitive touch input. Apps are powered by QuickJS, a lightweight JavaScript engine.
Applications can be:
- Embedded - compiled into the firmware image (built-in system apps)
- User-space - placed on the SD card in
/applications/and loaded at runtime
User-space apps appear on the home screen alongside built-in apps. They have full access to all Oriel APIs. On the home screen, Oriel API apps appear in teal and p5.js apps appear in purple.
Quick Start
The fastest way to get an app running:
- Open the Oriel Admin interface and connect to your device via USB serial
- Switch to the Apps tab
- Select a template or write your code in the editor
- Click Deploy to Device
- Your app appears on the home screen immediately
Alternatively, place any .js file in /sdcard/applications/ on the SD card directly.
App Structure
Every app is a single .js file. The filename (without .js) becomes the app identifier.
Metadata Header
Apps should start with an optional metadata block that provides a display name and description:
/*===JS_METADATA
name: "My App Name"
version: "1.0.0"
author: "Your Name"
description: "What this app does"
permissions: ["system", "sensors"]
entry_point: "setup"
===*/
The name field is shown on the home screen. Without metadata, the filename is used (e.g., my_app.js displays as "My App").
Three Ways to Build Apps
Oriel supports three styles of writing applications. All three have access to the same hardware and sensors — choose the one that fits your project and experience.
| Style | Best For | Feels Like | Runtime |
|---|---|---|---|
| Oriel API | Custom UIs, dashboards, full control | Canvas/HTML5 drawing | Default |
| p5.js | Generative art, creative coding, visual experiments | Processing/p5.js | runtime: "p5" |
| Declarative UI | Settings, forms, lists, data display | SwiftUI / React | Default (uses UIApp framework) |
Oriel API Style
Direct hardware access via the Oriel.display.* namespace. You draw every pixel yourself. Most flexible, best for custom interfaces.
Oriel.display.clearScreen("#000000");
Oriel.display.drawFilledCircle(180, 180, 50, "#FF0000");
Oriel.display.drawText(120, 280, "Hello!", "#FFFFFF", 3);
p5.js Style
p5.js is a popular JavaScript library for creative coding, used by artists, designers, students, and educators worldwide. If you've used p5.js or Processing before, you can write Oriel apps the same way. Just add runtime: "p5" to your metadata header and the compatibility layer loads automatically.
Learn more about p5.js at p5js.org — the reference and examples are a great starting point.
/*===JS_METADATA
name: "My Sketch"
runtime: "p5"
===*/
function setup() {
createCanvas(466, 466);
background(0);
}
function draw() {
fill(255, 0, 0);
circle(mouseX, mouseY, 30);
}
runtime: "p5" is set, the engine automatically loads the p5 compatibility layer before your sketch, then calls your setup() and starts the draw() loop. This provides the p5 global functions (fill, stroke, circle, rect, etc.), mouse/touch mapping (mouseX, mouseY, mouseIsPressed), and the draw loop. Native C++ implementations of p5 drawing functions ensure high performance.
Declarative UI Style
Build interfaces declaratively using the built-in UIApp framework (inspired by SwiftUI). Components like VStack, HStack, Text, Button, and ScrollView handle layout and touch automatically.
var app;
function buildUI() {
return new VStack([
new Text("Settings", { style: "title", color: "#FFD700" }),
new Spacer(10),
new Button("Do Something", function() {
Oriel.system.logInfo("Tapped!");
app.setNeedsRedraw();
}, { color: "#58a6ff" }),
], { spacing: 10, align: "center", padding: 20 });
}
function setup() {
app = new UIApp(buildUI, {
bounds: { x: 20, y: 20, width: 320, height: 320 },
backgroundColor: "#000000"
});
}
function draw() {
app.render(); // Framework handles everything
}
The UIApp framework is loaded globally at engine startup — no imports needed. See the Declarative UI template for a complete example.
Available UI Components
| Component | Description |
|---|---|
VStack(children, opts) | Vertical layout. Options: spacing, align ("left"/"center"/"right"), padding |
HStack(children, opts) | Horizontal layout. Options: spacing, align ("top"/"center"/"bottom"), padding |
ScrollView(children, opts) | Scrollable vertical list with momentum. Add stateId for scroll position persistence |
Text(label, opts) | Text label. label can be a string or () => string function for dynamic text. Options: size (1-4), style ("title"/"body"/"caption"/etc.), color |
Button(label, onClick, opts) | Tappable button. Options: color, textColor, padding, size |
Circle(opts) | Circle shape. Options: radius, color, filled |
Spacer(height) | Empty space of given height |
Reactive State
// Create observable state
var count = observable(0);
// Use in dynamic text
new Text(function() { return "Count: " + count.value; }, { size: 3 })
// Update from button
new Button("+1", function() {
count.set(count.value + 1);
app.setNeedsRedraw();
}, { color: "#3fb950" })
App Patterns
Oriel supports two app patterns. Choose based on your needs:
Pattern 1: setup() / draw() (Recommended)
Best for: continuous animations, real-time displays, games, interactive UIs.
function setup() {
// Called once when app starts
Oriel.display.clearScreen("#000000");
Oriel.display.drawText(100, 180, "Loading...", "#FFFFFF", 3);
}
function draw() {
// Called every frame (~30 FPS)
// Read input, update state, draw screen
var touch = Oriel.sensors.readTouch();
if (touch.active) {
Oriel.display.drawFilledCircle(touch.x, touch.y, 10, "#FF0000");
}
}
The engine automatically calls setup() once, then calls draw() in a loop at approximately 30 FPS. The display buffer is flushed automatically after each draw() call. You just define the functions — the engine handles the rest.
Pattern 2: main() Loop
Best for: simple utilities, one-shot displays, sequential workflows.
function main() {
Oriel.display.clearScreen("#000000");
Oriel.display.drawText(100, 100, "Hello!", "#FFFFFF", 3);
Oriel.display.refreshDisplay();
// Run for 30 seconds
for (var i = 0; i < 300; i++) {
var touch = Oriel.sensors.readTouch();
if (touch.active) break; // Exit on touch
Oriel.system.sleepMs(100);
}
}
main();
In this pattern, you manage your own loop and timing with Oriel.system.sleepMs(). Call Oriel.display.refreshDisplay() after drawing to push the buffer to screen.
Exiting Apps
Users exit apps with a long-press gesture (hold 3 fingers). The engine handles this automatically for setup()/draw() apps. For main() apps, check the touch exit gesture:
var touch = Oriel.sensors.readTouch();
if (touch.exitGesture) {
return; // Exit app
}
Sleep Callback (Optional)
Apps can respond to the device entering sleep mode:
function willSleep(sleepInfo) {
// Device is about to sleep - pause animations, save state, etc.
Oriel.system.logInfo("Going to sleep, saving state...");
}
Deploying Apps
There are three ways to deploy user-space apps:
| Method | How | Best For |
|---|---|---|
| Admin Interface | Use the Apps tab in the admin UI to write and deploy over USB serial | Development and testing |
| SD Card | Copy .js files to /applications/ on the SD card |
Distributing apps |
| File Upload | Use the SD Card tab to upload files to /sdcard/applications/ |
Quick file transfer |
After deploying, the app appears on the home screen the next time you return to the launcher (exit current app or reboot).
Template: Hello World
Minimal App
Displays a message and exits on touch. Good starting point for any app.
/*===JS_METADATA
name: "Hello World"
version: "1.0.0"
description: "A minimal Oriel app"
entry_point: "setup"
===*/
var needsRedraw = true;
function setup() {
Oriel.display.clearScreen("#000000");
Oriel.display.drawText(110, 160, "Hello, Oriel!", "#FFFFFF", 3);
Oriel.display.drawText(100, 200, "Tap to go back", "#58a6ff", 2);
}
function draw() {
var touch = Oriel.sensors.readTouch();
if (touch.active) {
Oriel.system.exitApp();
}
}
Template: Drawing App
Touch Drawing Canvas
Draw on screen with touch. Shows touch handling, state tracking, and continuous rendering.
/*===JS_METADATA
name: "Draw"
version: "1.0.0"
description: "Touch drawing canvas"
entry_point: "setup"
===*/
var lastX = -1;
var lastY = -1;
var wasActive = false;
var hue = 0;
function hslToHex(h, s, l) {
h = h / 360;
var r, g, b;
if (s === 0) {
r = g = b = l;
} else {
function hue2rgb(p, q, t) {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t;
if (t < 1/2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
return p;
}
var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
var p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3);
}
var ri = Math.round(r * 255);
var gi = Math.round(g * 255);
var bi = Math.round(b * 255);
return "#" + ((1 << 24) + (ri << 16) + (gi << 8) + bi).toString(16).slice(1);
}
function setup() {
Oriel.display.clearScreen("#111111");
Oriel.display.drawText(100, 170, "Touch to draw", "#FFFFFF", 3);
Oriel.display.drawText(100, 200, "Colors change", "#58a6ff", 2);
}
function draw() {
var touch = Oriel.sensors.readTouch();
if (touch.active) {
var color = hslToHex(hue, 1.0, 0.5);
if (wasActive && lastX >= 0) {
// Draw line from last point for smooth strokes
Oriel.display.drawWideLine(lastX, lastY, touch.x, touch.y, 4, color);
} else {
Oriel.display.drawFilledCircle(touch.x, touch.y, 3, color);
}
lastX = touch.x;
lastY = touch.y;
hue = (hue + 1) % 360;
} else {
lastX = -1;
lastY = -1;
}
wasActive = touch.active;
}
Template: Sensor Dashboard
Battery & WiFi Dashboard
Reads battery and WiFi status. Shows sensor APIs, conditional rendering, and periodic updates.
/*===JS_METADATA
name: "Dashboard"
version: "1.0.0"
description: "Battery and WiFi status dashboard"
permissions: ["sensors", "network"]
entry_point: "setup"
===*/
var frameCounter = 0;
function setup() {
Oriel.display.clearScreen("#000000");
}
function draw() {
// Update every 30 frames (~1 second at 30fps)
frameCounter++;
if (frameCounter % 30 !== 1) return;
Oriel.display.clearScreen("#0d1117");
// Title
Oriel.display.drawText(120, 30, "Dashboard", "#FFFFFF", 3);
// Battery info
var bat = Oriel.sensors.readBattery();
var batColor = bat.percentage > 50 ? "#3fb950" : bat.percentage > 20 ? "#d29922" : "#f85149";
Oriel.display.drawText(40, 80, "Battery", "#8b949e", 2);
Oriel.display.drawText(40, 105, bat.percentage + "%", batColor, 4);
Oriel.display.drawText(40, 140, bat.voltage.toFixed(2) + "V", "#8b949e", 2);
Oriel.display.drawText(40, 160, bat.charging ? "Charging" : "On battery", "#8b949e", 2);
// Progress bar for battery
Oriel.display.drawProgressBar(40, 185, 280, 12, bat.percentage / 100, batColor, "#30363d", "#30363d");
// WiFi info
var wifi = Oriel.network.getWifiStatus();
var wifiColor = wifi.connected ? "#3fb950" : "#f85149";
Oriel.display.drawText(40, 220, "WiFi", "#8b949e", 2);
Oriel.display.drawText(40, 245, wifi.connected ? "Connected" : "Disconnected", wifiColor, 3);
if (wifi.connected) {
Oriel.display.drawText(40, 270, wifi.ssid, "#e6edf3", 2);
Oriel.display.drawText(40, 290, wifi.ip_address, "#8b949e", 2);
Oriel.display.drawText(40, 310, wifi.signal_strength + " dBm", "#8b949e", 2);
}
// Exit hint
Oriel.display.drawText(100, 340, "Hold 3 fingers to exit", "#30363d", 2);
}
Template: Menu UI
Interactive Menu
Scrollable menu with touch selection. Shows list rendering, touch hit-testing, and screen transitions.
/*===JS_METADATA
name: "Menu Demo"
version: "1.0.0"
description: "Interactive menu with touch selection"
entry_point: "setup"
===*/
var items = [
{ label: "Option A", color: "#58a6ff" },
{ label: "Option B", color: "#3fb950" },
{ label: "Option C", color: "#d29922" },
{ label: "Option D", color: "#f85149" },
{ label: "Option E", color: "#bc8cff" },
];
var selected = -1;
var screen = "menu"; // "menu" or "detail"
var needsRedraw = true;
var lastTouch = false;
function drawMenu() {
Oriel.display.clearScreen("#0d1117");
Oriel.display.drawText(130, 30, "Select One", "#FFFFFF", 3);
for (var i = 0; i < items.length; i++) {
var y = 80 + i * 52;
var bg = (i === selected) ? "#1c2129" : "#161b22";
Oriel.display.drawFilledRoundedRect(30, y, 300, 44, 8, bg);
Oriel.display.drawRoundedRect(30, y, 300, 44, 8, items[i].color);
Oriel.display.drawText(50, y + 15, items[i].label, "#FFFFFF", 3);
}
}
function drawDetail() {
var item = items[selected];
Oriel.display.clearScreen("#0d1117");
Oriel.display.drawFilledCircle(180, 150, 50, item.color);
Oriel.display.drawText(100, 230, item.label, "#FFFFFF", 4);
Oriel.display.drawText(110, 280, "Tap to go back", "#8b949e", 2);
}
function setup() {
needsRedraw = true;
}
function draw() {
var touch = Oriel.sensors.readTouch();
var justPressed = touch.active && !lastTouch;
lastTouch = touch.active;
if (justPressed) {
if (screen === "detail") {
screen = "menu";
selected = -1;
needsRedraw = true;
} else {
// Check which menu item was tapped
for (var i = 0; i < items.length; i++) {
var y = 80 + i * 52;
if (touch.x >= 30 && touch.x <= 330 && touch.y >= y && touch.y <= y + 44) {
selected = i;
screen = "detail";
needsRedraw = true;
break;
}
}
}
}
if (needsRedraw) {
if (screen === "menu") drawMenu();
else drawDetail();
needsRedraw = false;
}
}
Template: Animation
Animated Circles
Smooth animation with frame counting and trigonometry. Shows the draw loop pattern at its best.
/*===JS_METADATA
name: "Rings"
version: "1.0.0"
description: "Animated ring pattern"
entry_point: "setup"
===*/
var frame = 0;
function setup() {
Oriel.display.clearScreen("#000000");
}
function draw() {
Oriel.display.clearScreen("#000000");
frame++;
var cx = 180;
var cy = 180;
// Draw concentric pulsing rings
for (var i = 0; i < 8; i++) {
var phase = frame * 0.03 + i * 0.8;
var radius = 20 + i * 22 + Math.sin(phase) * 10;
var brightness = Math.floor(80 + Math.sin(phase + 1) * 60);
// Color shifts with each ring
var r = Math.floor(brightness * (0.5 + 0.5 * Math.sin(frame * 0.02 + i)));
var g = Math.floor(brightness * (0.5 + 0.5 * Math.sin(frame * 0.02 + i + 2)));
var b = Math.floor(brightness * (0.5 + 0.5 * Math.sin(frame * 0.02 + i + 4)));
var hex = "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
Oriel.display.drawCircle(cx, cy, radius, hex);
}
// Orbiting dot
var angle = frame * 0.05;
var dotX = cx + Math.cos(angle) * 120;
var dotY = cy + Math.sin(angle) * 120;
Oriel.display.drawFilledCircle(dotX, dotY, 8, "#FFFFFF");
}
Template: p5.js Basic Sketch
Interactive p5.js Sketch
A standard p5.js sketch with touch interaction. If you know p5.js, you already know how to write this.
/*===JS_METADATA
name: "p5 Sketch"
version: "1.0.0"
description: "Interactive p5.js sketch"
runtime: "p5"
===*/
var colors = [];
function setup() {
createCanvas(466, 466);
background(20);
noStroke();
for (var i = 0; i < 6; i++) {
colors.push([
Math.floor(Math.random() * 255),
Math.floor(Math.random() * 255),
Math.floor(Math.random() * 255)
]);
}
}
function draw() {
// Slow fade effect
fill(20, 20, 20, 15);
rect(0, 0, width, height);
// Draw orbiting circles
for (var i = 0; i < colors.length; i++) {
var angle = frameCount * 0.02 + i * (Math.PI * 2 / colors.length);
var radius = 80 + Math.sin(frameCount * 0.01 + i) * 30;
var x = width / 2 + Math.cos(angle) * radius;
var y = height / 2 + Math.sin(angle) * radius;
var size = 20 + Math.sin(frameCount * 0.03 + i * 2) * 10;
fill(colors[i][0], colors[i][1], colors[i][2], 200);
circle(x, y, size);
}
// Touch interaction
if (mouseIsPressed) {
fill(255, 255, 255, 180);
circle(mouseX, mouseY, 40);
// Draw lines to all orbiting circles
stroke(255, 100);
strokeWeight(1);
for (var i = 0; i < colors.length; i++) {
var angle = frameCount * 0.02 + i * (Math.PI * 2 / colors.length);
var radius = 80 + Math.sin(frameCount * 0.01 + i) * 30;
var x = width / 2 + Math.cos(angle) * radius;
var y = height / 2 + Math.sin(angle) * radius;
line(mouseX, mouseY, x, y);
}
noStroke();
}
}
Template: p5.js Particles
Particle System
Touch to emit particles. Shows arrays, physics simulation, and the p5 draw loop.
/*===JS_METADATA
name: "Particles"
version: "1.0.0"
description: "Touch-driven particle system"
runtime: "p5"
===*/
var particles = [];
var MAX_PARTICLES = 80;
function setup() {
createCanvas(466, 466);
}
function draw() {
background(10, 10, 20);
// Spawn particles on touch
if (mouseIsPressed && particles.length < MAX_PARTICLES) {
for (var j = 0; j < 3; j++) {
particles.push({
x: mouseX,
y: mouseY,
vx: (Math.random() - 0.5) * 6,
vy: (Math.random() - 0.5) * 6 - 2,
life: 1.0,
r: Math.floor(Math.random() * 200 + 55),
g: Math.floor(Math.random() * 100 + 50),
b: Math.floor(Math.random() * 255),
size: Math.random() * 8 + 4
});
}
}
// Update and draw particles
noStroke();
for (var i = particles.length - 1; i >= 0; i--) {
var p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.vy += 0.08; // gravity
p.life -= 0.015;
if (p.life <= 0) {
particles.splice(i, 1);
continue;
}
var alpha = Math.floor(p.life * 255);
fill(p.r, p.g, p.b, alpha);
circle(p.x, p.y, p.size * p.life);
}
// Info text
fill(255);
textSize(12);
text("Touch to create particles", 100, 340);
text("Particles: " + particles.length, 140, 20);
}
Template: Declarative UI
Settings Panel with Declarative UI
A settings-style interface using VStack, HStack, Button, and reactive state. No manual drawing needed.
/*===JS_METADATA
name: "My Settings"
version: "1.0.0"
description: "Declarative UI settings panel"
entry_point: "setup"
===*/
var app;
var brightness = 80;
var counter = observable(0);
function buildUI() {
return new ScrollView([
new Text("My Settings", { style: "title", color: "#FFD700" }),
new Spacer(10),
// Brightness control
new Text(function() { return "Brightness: " + brightness + "%"; }, { size: 2 }),
new HStack([
new Button("-10", function() {
if (brightness > 0) {
brightness = Math.max(0, brightness - 10);
Oriel.display.setBacklight(Math.floor(brightness * 2.55));
app.setNeedsRedraw();
}
}, { color: "#f85149", padding: 8 }),
new Button("+10", function() {
if (brightness < 100) {
brightness = Math.min(100, brightness + 10);
Oriel.display.setBacklight(Math.floor(brightness * 2.55));
app.setNeedsRedraw();
}
}, { color: "#3fb950", padding: 8 }),
], { spacing: 10, align: "center" }),
new Spacer(15),
// Counter with observable
new Text(function() { return "Counter: " + counter.value; },
{ size: 3, color: "#58a6ff" }),
new HStack([
new Button("-1", function() {
counter.set(counter.value - 1);
app.setNeedsRedraw();
}, { color: "#d29922", padding: 8 }),
new Button("+1", function() {
counter.set(counter.value + 1);
app.setNeedsRedraw();
}, { color: "#58a6ff", padding: 8 }),
new Button("Reset", function() {
counter.set(0);
app.setNeedsRedraw();
}, { color: "#8b949e", padding: 8 }),
], { spacing: 8, align: "center" }),
new Spacer(20),
// Battery info
new Text("Device Info", { style: "headline", color: "#bc8cff" }),
new Text(function() {
var bat = Oriel.sensors.readBattery();
return "Battery: " + bat.percentage + "% (" + bat.voltage.toFixed(2) + "V)";
}, { size: 2, color: "#8b949e" }),
new Text(function() {
var wifi = Oriel.network.getWifiStatus();
return "WiFi: " + (wifi.connected ? wifi.ssid : "Disconnected");
}, { size: 2, color: "#8b949e" }),
new Spacer(20),
new Button("Exit", function() {
Oriel.system.exitApp();
}, { color: "#f85149", padding: 10 }),
], { spacing: 8, align: "center", padding: 15, stateId: "my_settings_scroll" });
}
function setup() {
app = new UIApp(buildUI, {
bounds: { x: 10, y: 10, width: 340, height: 340 },
backgroundColor: "#0d1117"
});
}
function draw() {
app.render();
}
API: Oriel.display
All drawing functions for the 466x466 display.
Screen Management
clearScreen(color)Fill the entire screen with a color. Call at the start of each frame for full redraws.
refreshDisplay()Flush the draw buffer to the screen. Automatic in draw() pattern; call manually in main() pattern.
Set screen brightness. 0 = off, 255 = maximum.
getWidth() -> numberReturns display width (466).
getHeight() -> numberReturns display height (466).
Text
drawText(x, y, text, color, size, bgColor?)Draw text. size: 1-5 for scale factor. bgColor is optional (transparent if omitted).
Rectangles
drawRect(x, y, width, height, color)Draw rectangle outline.
drawFilledRect(x, y, width, height, color)Draw filled rectangle.
drawRoundedRect(x, y, width, height, radius, color)Draw rounded rectangle outline.
drawFilledRoundedRect(x, y, width, height, radius, color)Draw filled rounded rectangle.
Circles
drawCircle(x, y, radius, color)Draw circle outline.
drawFilledCircle(x, y, radius, color)Draw filled circle.
Lines & Triangles
drawLine(x1, y1, x2, y2, color)Draw a 1-pixel line between two points.
drawWideLine(x1, y1, x2, y2, width, color)Draw a line with specified thickness.
drawTriangle(x1, y1, x2, y2, x3, y3, color)Draw triangle outline.
drawFilledTriangle(x1, y1, x2, y2, x3, y3, color)Draw filled triangle.
Advanced
drawGradient(x, y, width, height, color1, color2, vertical)Draw gradient fill. vertical: 0 = horizontal, 1 = vertical.
Draw a progress bar. progress: 0.0 to 1.0.
Draw an image from the SD card (JPG, PNG, BMP). Width/height are optional for scaling.
API: Oriel.sensors
Touch
readTouch() -> { x, y, active, pressed, gesture, exitGesture }Read current touch state. active: finger is touching. exitGesture: user wants to exit.
Battery
readBattery() -> { voltage, percentage, charging, healthStatus, ... }Read battery state. percentage: 0-100. voltage: in volts. charging: boolean.
Battery voltage in volts.
getBatteryPercentage() -> numberBattery percentage 0-100.
IMU (Accelerometer & Gyroscope)
readGyro() -> { accel_x, accel_y, accel_z, gyro_x, gyro_y, gyro_z, temperature }Read motion sensor. Accelerometer in g units, gyroscope in degrees/sec.
getTemperature() -> numberIMU sensor temperature in Celsius.
API: Oriel.touch
read() -> { x, y, active, pressed, gesture, exitGesture }Same as Oriel.sensors.readTouch(). Alternative namespace for touch input.
API: Oriel.filesystem
readFile(path) -> stringRead a file's contents as a string. Checks embedded files first, then SD card.
writeFile(path, content) -> booleanWrite string content to a file on the SD card.
listFiles(path) -> string[]List files in a directory. Returns filenames; directories end with /.
Load and execute a JavaScript file in the current context.
API: Oriel.system
logInfo(message)Log an info message (visible in serial monitor).
logWarn(message)Log a warning.
logError(message)Log an error.
logDebug(message)Log a debug message.
sleepMs(milliseconds)Pause execution for N milliseconds. Use in main() pattern for timing.
Launch another app by name. The current app is cleaned up first.
exitApp()Exit the current app and return to the home screen.
getCurrentApp() -> stringGet the name of the currently running app.
startRenderLoop(fps?) -> booleanStart the render loop. Default 30 FPS. Range: 1-60.
stopRenderLoop()Stop the render loop.
resetSleepTimer()Reset the inactivity timer to prevent device sleep.
shutdown()Power off the device via PMIC.
API: Oriel.network
initWifi() -> booleanInitialize WiFi (runs in background). Called automatically by the launcher.
getWifiStatus() -> { status, connected, ssid, ip_address, signal_strength, error_message }Get current WiFi connection state. signal_strength is RSSI in dBm.
Connect to a WiFi network.
connectFromConfig() -> booleanConnect using saved credentials from /sdcard/wifi_config.toml.
Disconnect from WiFi.
scanNetworks() -> [{ ssid, signal_strength, security, channel }]Scan for available WiFi networks.
API: Oriel.audio
Playback
playFile(filepath) -> booleanPlay an audio file from the SD card.
stop() -> booleanStop playback.
pause() -> booleanPause playback.
resume() -> booleanResume paused playback.
isPlaying() -> booleanCheck if audio is currently playing.
getInfo() -> { state, current_file, volume, position_ms, duration_ms }Get playback info. state: "idle", "playing", "paused", "error".
List audio files. Defaults to /sdcard.
Volume
setVolume(volume)Set volume, 0.0 to 1.0.
getVolume() -> numberGet current volume.
setHeadphoneMode(enabled)Enable or disable headphone output.
isHeadphoneMode() -> booleanCheck if headphone mode is active.
Microphone
startMicrophone() -> booleanStart listening on the microphone.
stopMicrophone() -> booleanStop the microphone.
getAudioLevel() -> numberGet mic input level, 0-100.
startRecording() -> booleanStart recording audio.
stopRecording() -> booleanStop recording.
isRecording() -> booleanCheck if recording is active.
API: Oriel.rtc
getDateTime() -> { year, month, day, day_of_week, hour, minute, second, formatted }Get current date and time from the hardware real-time clock.
getTimeString() -> stringFormatted time, e.g., "14:30:45".
getDateString() -> stringFormatted date, e.g., "2024-03-15".
setDateTime(year, month, day, hour, minute, second) -> booleanSet the RTC time.
syncFromNTP() -> booleanSync time from an NTP server (requires WiFi).
syncFromSystem() -> booleanSync RTC from the ESP32 system clock.
needsSync() -> booleanCheck if the RTC needs time synchronization.
isRunning() -> booleanCheck if the RTC oscillator is running.
getStatus() -> { rtc_running, alarm_enabled, alarm_flag, current_time, last_sync_source, ... }Full RTC status object.
setAlarm(hour, minute, second) -> booleanSet an alarm (24-hour format).
clearAlarm() -> booleanClear the alarm flag.
getAlarmFlag() -> booleanCheck if the alarm has triggered.
API: Oriel.gps
start() -> booleanInitialize and start the GPS module.
stop()Stop GPS and enter sleep mode.
poll() -> booleanPoll for new GPS data. Call periodically (every ~30 seconds).
getPosition() -> { latitude, longitude, altitude, speed, course, valid }Get current position. altitude in meters, speed in m/s, course in degrees.
Get GPS fix status and satellite info.
getRawNMEA() -> stringGet raw NMEA sentences for debugging.
API: Oriel.stt
Speech-to-text with Claude AI responses. Requires WiFi and Oriel Cloud authentication.
process() -> booleanProcess recorded audio: transcribe and get an AI response.
isProcessing() -> booleanCheck if STT is currently processing.
getResult() -> { state, transcription, answer, error }state: "idle", "processing", "done", "error".
Reset STT state for a new recording.
P5.js Compatibility Layer
Oriel includes a p5.js compatibility shim. These functions are available globally (no Oriel. prefix). For the full p5.js reference, see p5js.org/reference.
Drawing State
background(r, g, b) // Set background color
fill(r, g, b) // Set fill color
noFill() // Disable fill
stroke(r, g, b) // Set stroke color
noStroke() // Disable stroke
strokeWeight(w) // Set line width
Shapes
rect(x, y, w, h) // Rectangle
square(x, y, size) // Square
circle(x, y, diameter) // Circle
ellipse(x, y, w, h) // Ellipse
line(x1, y1, x2, y2) // Line
point(x, y) // Single pixel
triangle(x1,y1,x2,y2,x3,y3)
arc(x, y, w, h, start, stop)
bezier(x1,y1,x2,y2,x3,y3,x4,y4)
Custom Shapes
beginShape() // Start shape
vertex(x, y) // Add point
endShape(close) // End shape (1=closed)
Transformations
push() // Save state
pop() // Restore state
translate(x, y) // Move origin
rotate(angle) // Rotate (radians)
scale(sx, sy) // Scale
Text
text(str, x, y) // Draw text
textSize(size) // Set text size
textAlign(align) // Set alignment
Global Variables
width // Display width (466)
height // Display height (466)
frameCount // Frames rendered
mouseX, mouseY // Touch position
mouseIsPressed // Touch active
Colors
Colors can be specified in two formats:
| Format | Example | Notes |
|---|---|---|
| Hex string | "#FF0000" | Most readable, recommended |
| Hex number | 0xFF0000 | Slightly faster |
Common colors:
"#000000" // Black
"#FFFFFF" // White
"#FF0000" // Red
"#00FF00" // Green
"#0000FF" // Blue
"#FFFF00" // Yellow
"#FF8800" // Orange
"#8b949e" // Gray (dim text)
"#58a6ff" // Accent blue
"#3fb950" // Success green
"#f85149" // Error red
"#d29922" // Warning yellow
Coordinates & Layout
The display is 466x466 pixels. Origin (0,0) is the top-left corner.
0 233 466
0 +-------------------+------------------+
| | |
| Visible area is a circle |
| centered at (233, 233) |
| with radius 233 |
233 + (center) +
| |
| Corners are NOT visible |
| on the round display |
| |
466 +--------------------------------------+
Tips for the round screen:
- Keep important content within a ~420px diameter circle centered at (233, 233)
- Text near edges may be clipped by the circular bezel
- Use
Math.sqrt(dx*dx + dy*dy) < 233to check if a point is visible - Use
widthandheightglobals instead of hardcoded values for cross-device compatibility
Performance Tips
- Minimize redraws: Use a
needsRedrawflag and only redraw when state changes - Clear once: Call
clearScreen()once per frame, not for each element - Avoid string concatenation in hot loops: QuickJS handles strings efficiently but excessive concatenation adds GC pressure
- Use
sleepMs()in main() loops: Without delays, the loop will consume 100% CPU - Batch drawing operations: The display buffer accumulates draws and flushes once per frame
- Keep apps under ~50KB: Larger files take longer to parse and use more PSRAM
- Avoid deep recursion: The JavaScript stack is limited (512KB but shared with PSRAM)
Troubleshooting
App doesn't appear on home screen
- Check the file is in
/sdcard/applications/(not a subdirectory) - File must have
.jsextension - Return to home screen (exit current app) to trigger re-scan
App fails to launch
- Check serial log for JavaScript errors
- Ensure the file has Unix line endings (LF, not CRLF)
- Avoid special UTF-8 characters that QuickJS may not handle
- Make sure
setup()anddraw()are defined as global functions
"Failed to load" error
- File might be empty or unreadable
- SD card might not be mounted - check serial log for mount errors
Display shows nothing
- For
main()pattern: callOriel.display.refreshDisplay()after drawing - For
setup()/draw()pattern: flush is automatic - make suredraw()is defined
Touch not working
- Use
Oriel.sensors.readTouch()(notOriel.touch.read()) for consistency - Implement edge detection: check
touch.active && !lastTouchStatefor tap events
App crashes or device reboots
- Check for infinite loops without
sleepMs()(starves the watchdog timer) - Large array/object allocations may exhaust PSRAM
- Use
Oriel.system.logInfo()to trace execution