KingKiosk Custom Widget SDK¶
Build custom widgets for KingKiosk using standard web technologies (HTML, CSS, JavaScript). Widgets run either in a local customWebView tile (InAppWebView bridge) or through the Remote Browser custom-widget bridge, and can communicate with the KingKiosk platform through the window.KingKiosk JavaScript API.
Table of Contents¶
- Quick Start
- Adding a Widget
- JavaScript Bridge API
- Receiving Commands
- Sending Commands
- Publishing Telemetry
- Persistent Storage
- Widget Info
- MQTT Topics
- Complete Example
- Best Practices
- Troubleshooting
Quick Start¶
Create a simple widget in 3 steps:
1. Create your widget HTML¶
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My Widget</title>
<style>
body {
margin: 0;
padding: 20px;
background: #1a1a2e;
color: white;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
display: flex;
align-items: center;
justify-content: center;
min-height: 100vh;
}
.value {
font-size: 72px;
font-weight: bold;
}
</style>
</head>
<body>
<div class="value" id="display">--</div>
<script>
// Wait for the KingKiosk bridge to be ready
window.addEventListener('kingkiosk-ready', function() {
console.log('KingKiosk bridge ready!');
// Listen for commands from the platform
window.KingKiosk.onCommand(function(command, payload) {
if (command === 'set_value') {
document.getElementById('display').textContent = payload.value;
}
});
});
</script>
</body>
</html>
2. Host your widget (or use inline HTML)¶
Option A: Host on a web server
Option B: Use inline HTML (no hosting required) - Pass the HTML directly via MQTT command
3. Add the widget via MQTT¶
{
"command": "create_remote_browser",
"window_id": "my_widget_1",
"name": "My Widget",
"initial_url": "https://your-server.com/widgets/my-widget/",
"auto_connect": true
}
Adding a Widget¶
There are two supported runtime paths today:
Option A: Remote Browser custom widget bridge (recommended, required on tvOS)¶
Create a remote browser window pointed at your widget URL:
{
"command": "create_remote_browser",
"window_id": "weather_widget_1",
"name": "Weather Widget",
"initial_url": "https://widgets.example.com/weather/",
"auto_connect": true
}
Optional override (usually not needed): include server_url to force a specific Feature Server endpoint.
Option B: Reconfigure an existing local customWebView tile¶
Current dispatcher code does not expose a dedicated system command that creates a new local customWebView tile directly. If you already have one (for example from restored state), configure it via the window command topic:
Topic: kingkiosk/{device_id}/window/{window_id}/command
{
"action": "configure",
"url": "https://widgets.example.com/weather/",
"title": "Weather",
"storage": {
"city": "San Francisco",
"units": "fahrenheit"
}
}
Local customWebView with inline HTML (simple widgets, no hosting)¶
{
"action": "configure",
"html": "<!DOCTYPE html><html><body><h1 id='count'>0</h1><script>window.addEventListener('kingkiosk-ready',()=>{window.KingKiosk.onCommand((cmd,p)=>{if(cmd==='increment')document.getElementById('count').textContent=parseInt(document.getElementById('count').textContent)+1;});});</script></body></html>"
}
Local customWebView with base64-encoded HTML (special characters, larger widgets)¶
{
"action": "configure",
"html_base64": "PCFET0NUWVBFIGh0bWw+PGh0bWw+PGJvZHk+PGgxPkhlbGxvIFdvcmxkPC9oMT48L2JvZHk+PC9odG1sPg=="
}
Feature Server auto-registration API (optional, alongside inline)¶
Use this when you want dynamic HTML without public hosting, or when a strict browser/runtime rejects inline data: style content.
This uploads your widget HTML to the Feature Server and returns a hosted URL you can use with either:
- create_remote_browser (initial_url), or
- local customWebView configure (url)
Why you might need it¶
- Inline is blocked by runtime policy: some widget content (especially embedded media/providers) may reject inline/opaque origins.
- CSP/X-Frame requirements: hosted HTTP(S) origin can satisfy restrictions that inline content cannot.
- Large payload reliability: avoids pushing large HTML blobs through MQTT command payloads.
- Reuse across devices/windows: one registered URL can be reused by multiple widget windows.
- Operational visibility: registration is a server-side step you can audit/log independently of MQTT.
Quick decision guide¶
- Use inline
htmlfor small/simple widgets and fast iteration. - Use inline
html_base64for medium widgets with escaping-heavy markup. - Use Feature Server auto-registration API when inline fails due to origin/security constraints or payload size/operational concerns.
How to use it¶
- Prepare your widget HTML as a string/file.
- Base64 encode the HTML.
POSTto Feature Server:http://<feature-server-host>:4000/api/browser/custom-widget/register- Read
urlfrom the response. - Use that URL in your widget command.
API request¶
{
"widgetId": "weather_widget_1",
"pageKey": "weather_widget_1",
"htmlBase64": "<base64-encoded-html>"
}
API response (example)¶
{
"url": "http://192.168.0.199:4000/api/browser/custom-widget/27d44c512a94431e3e7378dc2db3a23a.html"
}
Example with curl¶
HTML_B64="$(base64 < my-widget.html | tr -d '\n')"
curl -sS -X POST "http://192.168.0.199:4000/api/browser/custom-widget/register" \
-H "Content-Type: application/json" \
-d "{
\"widgetId\":\"weather_widget_1\",
\"pageKey\":\"weather_widget_1\",
\"htmlBase64\":\"${HTML_B64}\"
}"
Use returned URL: Remote Browser¶
{
"command": "create_remote_browser",
"window_id": "weather_widget_1",
"name": "Weather Widget",
"initial_url": "http://192.168.0.199:4000/api/browser/custom-widget/27d44c512a94431e3e7378dc2db3a23a.html",
"auto_connect": true
}
Use returned URL: local customWebView reconfigure¶
Topic: kingkiosk/{device_id}/window/{window_id}/command
{
"action": "configure",
"url": "http://192.168.0.199:4000/api/browser/custom-widget/27d44c512a94431e3e7378dc2db3a23a.html",
"title": "Weather"
}
Window Configuration Options¶
| Field | Path | Description |
|---|---|---|
window_id |
create_remote_browser |
Required remote browser window ID. |
name |
create_remote_browser |
Display name for remote browser window. |
initial_url |
create_remote_browser |
URL to load in remote browser session. |
server_url |
create_remote_browser |
Optional Feature Server override (deprecated as required input). |
widgetId |
Feature Server register API | Widget identifier sent to POST /api/browser/custom-widget/register. |
pageKey |
Feature Server register API | Page key sent to POST /api/browser/custom-widget/register (commonly same as widgetId). |
htmlBase64 |
Feature Server register API | Base64 HTML payload sent to POST /api/browser/custom-widget/register. |
action |
window command topic | Use "configure" to reconfigure a local customWebView tile. |
url |
local configure action |
URL to load (mutually exclusive with html/html_base64). |
html |
local configure action |
Raw HTML content. |
html_base64 |
local configure action |
Base64-encoded HTML content. |
title |
local configure action |
Optional title override. |
storage |
local configure action |
Initial key-value storage map for the widget runtime. |
Content Size Limits¶
When using inline HTML or base64-encoded content for local customWebView, be aware of MQTT message size limits:
| Content Method | Recommended Max | Notes |
|---|---|---|
| URL | Unlimited | Widget hosted externally, only URL sent via MQTT |
| Inline HTML | 500KB | JSON escaping adds overhead |
| Base64 HTML | 375KB original | Becomes ~500KB after encoding (+33%) |
MQTT Broker Limits: - MQTT protocol maximum: 256MB per message - Most production brokers: 256KB - 1MB default - AWS IoT Core: 128KB limit - Mosquitto default: 256MB (often configured lower)
Recommendations:
- For simple widgets (< 50KB): Use inline html for convenience
- For medium widgets (50KB - 375KB): Use html_base64 to avoid JSON escaping issues
- For complex widgets (> 375KB): Use url and host your widget externally
- For dynamic widgets without external hosting: use Feature Server auto-registration API, then pass returned url
Base64 Encoding Example:
# Encode your widget HTML
cat my-widget.html | base64 > my-widget-base64.txt
# Check size (should be < 500KB after encoding)
wc -c my-widget-base64.txt
JavaScript Bridge API¶
The KingKiosk platform injects a window.KingKiosk object into your widget. Wait for the kingkiosk-ready event before using it.
window.addEventListener('kingkiosk-ready', function() {
// Bridge is now available
console.log('KingKiosk API ready');
});
API Reference¶
| Method | Description |
|---|---|
KingKiosk.onCommand(callback) |
Register to receive commands |
KingKiosk.sendCommand(cmd, payload) |
Send command to platform |
KingKiosk.publishTelemetry(data) |
Publish telemetry data |
KingKiosk.storage.get(key) |
Get stored value (async) |
KingKiosk.storage.set(key, value) |
Store a value |
KingKiosk.storage.getAll() |
Get all stored values (async) |
KingKiosk.getWidgetInfo() |
Get widget metadata (async) |
Receiving Commands¶
Register a callback to receive commands sent from the KingKiosk platform or MQTT.
window.KingKiosk.onCommand(function(command, payload) {
console.log('Received:', command, payload);
switch (command) {
case 'set_value':
updateDisplay(payload.value);
break;
case 'set_color':
document.body.style.backgroundColor = payload.color;
break;
case 'refresh':
fetchLatestData();
break;
default:
console.log('Unknown command:', command);
}
});
Sending Commands to Your Widget via MQTT¶
Local customWebView command path¶
Topic: kingkiosk/{device_id}/window/{window_id}/command
Use either explicit widget_command:
Or send any custom action directly (unknown actions are forwarded to the widget callback):
Note: local customWebView commands are dispatched only after the widget registers a callback with KingKiosk.onCommand(...).
Remote Browser custom widget bridge command path¶
Topic: kingkiosk/{device_id}/element/{remote_browser_window_id}/cmd
Use command: "widget_command" and one of the supported payload shapes:
Shape A:
Shape B:
Sending Commands¶
Send commands from your widget to the KingKiosk platform. These are published to MQTT for external systems to consume.
// Simple command
window.KingKiosk.sendCommand('button_pressed', { buttonId: 'start' });
// Command with data
window.KingKiosk.sendCommand('form_submitted', {
name: 'John Doe',
email: 'john@example.com',
timestamp: Date.now()
});
// Emit an integration/event command for external consumers
window.KingKiosk.sendCommand('navigate', { url: '/settings' });
MQTT Output¶
Commands are published to:
Payload format:
{
"type": "custom_command",
"widget_id": "widget_abc123",
"command": "button_pressed",
"payload": { "buttonId": "start" },
"timestamp": 1705432100000
}
Publishing Telemetry¶
Publish sensor data, metrics, or any telemetry from your widget.
// Simple value
window.KingKiosk.publishTelemetry({ temperature: 72.5 });
// Multiple metrics
window.KingKiosk.publishTelemetry({
cpu_usage: 45.2,
memory_used: 8192,
disk_free: 50000,
uptime_seconds: 86400
});
// Periodic telemetry
setInterval(function() {
window.KingKiosk.publishTelemetry({
heartbeat: true,
timestamp: Date.now()
});
}, 30000);
MQTT Output¶
Telemetry is published to:
Payload format:
Persistent Storage¶
Store and retrieve data using KingKiosk.storage.
- Local
customWebView: storage is kept in the controller runtime while the tile exists. - Remote Browser custom widget bridge: storage is persisted via app storage keys (
custom_widget_bridge_storage_v1:*) and restored across app restarts.
// Store a value
window.KingKiosk.storage.set('theme', 'dark');
window.KingKiosk.storage.set('lastUpdate', Date.now());
window.KingKiosk.storage.set('settings', { volume: 80, muted: false });
// Retrieve a value (async)
const theme = await window.KingKiosk.storage.get('theme');
console.log('Current theme:', theme); // 'dark'
// Get all stored values
const allData = await window.KingKiosk.storage.getAll();
console.log('All storage:', allData);
// { theme: 'dark', lastUpdate: 1705432100000, settings: { volume: 80, muted: false } }
Initial Storage¶
Pre-populate storage when adding the widget:
{
"action": "configure",
"url": "https://example.com/widget/",
"storage": {
"apiKey": "your-api-key",
"refreshInterval": 60000,
"theme": "dark"
}
}
Widget Info¶
Get information about the widget and platform.
const info = await window.KingKiosk.getWidgetInfo();
console.log(info);
// {
// widgetId: "widget_abc123",
// platform: "macos" // or "android", "ios", "windows", "linux", "web"
// }
Use this to adapt your widget for different platforms:
const info = await window.KingKiosk.getWidgetInfo();
if (info.platform === 'ios' || info.platform === 'android') {
// Touch-optimized interface
document.body.classList.add('touch-mode');
} else {
// Desktop-like layout
document.body.classList.add('desktop-mode');
}
MQTT Topics¶
Receiving Commands: Local customWebView (Platform -> Widget)¶
Topic: kingkiosk/{device_id}/window/{window_id}/command
Receiving Commands: Remote Browser custom widget bridge (Platform -> Widget)¶
Topic: kingkiosk/{device_id}/element/{remote_browser_window_id}/cmd
Widget Events (Widget -> Platform)¶
Topic: kingkiosk/{device_id}/widget/{widget_id}/event
{
"type": "custom_command",
"widget_id": "my_widget",
"command": "user_action",
"payload": { "action": "clicked" },
"timestamp": 1705432100000
}
Widget Telemetry (Widget -> Platform)¶
Topic: kingkiosk/{device_id}/widget/{widget_id}/telemetry
Widget State (remote browser bridge, retained)¶
Topic: kingkiosk/{device_id}/widget/{widget_id}/state
{
"widget_id": "my_widget",
"type": "custom_webview",
"has_command_handler": true,
"storage": {},
"timestamp": 1705432100000
}
Complete Example¶
A full-featured widget demonstrating all API features:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Smart Thermostat Widget</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
color: white;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
.thermostat {
text-align: center;
}
.temperature {
font-size: 96px;
font-weight: 200;
line-height: 1;
}
.temperature .unit {
font-size: 36px;
vertical-align: top;
}
.label {
font-size: 14px;
text-transform: uppercase;
letter-spacing: 2px;
opacity: 0.7;
margin-top: 8px;
}
.controls {
display: flex;
gap: 20px;
margin-top: 40px;
}
.btn {
width: 60px;
height: 60px;
border-radius: 50%;
border: 2px solid rgba(255,255,255,0.3);
background: rgba(255,255,255,0.1);
color: white;
font-size: 24px;
cursor: pointer;
transition: all 0.2s;
}
.btn:hover {
background: rgba(255,255,255,0.2);
transform: scale(1.1);
}
.btn:active {
transform: scale(0.95);
}
.status {
margin-top: 30px;
font-size: 12px;
opacity: 0.5;
}
.mode {
margin-top: 20px;
padding: 8px 16px;
background: rgba(255,255,255,0.1);
border-radius: 20px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 1px;
}
.mode.heating { background: rgba(255,100,100,0.3); }
.mode.cooling { background: rgba(100,100,255,0.3); }
.mode.off { background: rgba(100,100,100,0.3); }
</style>
</head>
<body>
<div class="thermostat">
<div class="temperature">
<span id="temp">--</span><span class="unit">°F</span>
</div>
<div class="label">Target Temperature</div>
<div class="controls">
<button class="btn" id="btnDown">−</button>
<button class="btn" id="btnUp">+</button>
</div>
<div class="mode" id="mode">OFF</div>
<div class="status" id="status">Connecting...</div>
</div>
<script>
// State
let targetTemp = 72;
let mode = 'off';
let widgetId = 'unknown';
// DOM elements
const tempDisplay = document.getElementById('temp');
const modeDisplay = document.getElementById('mode');
const statusDisplay = document.getElementById('status');
const btnUp = document.getElementById('btnUp');
const btnDown = document.getElementById('btnDown');
// Update display
function updateDisplay() {
tempDisplay.textContent = targetTemp;
modeDisplay.textContent = mode.toUpperCase();
modeDisplay.className = 'mode ' + mode;
}
// Send telemetry
function sendTelemetry() {
window.KingKiosk.publishTelemetry({
target_temperature: targetTemp,
mode: mode,
timestamp: Date.now()
});
}
// Initialize when bridge is ready
window.addEventListener('kingkiosk-ready', async function() {
statusDisplay.textContent = 'Connected';
// Get widget info
const info = await window.KingKiosk.getWidgetInfo();
widgetId = info.widgetId;
// Load saved state
const savedTemp = await window.KingKiosk.storage.get('targetTemp');
const savedMode = await window.KingKiosk.storage.get('mode');
if (savedTemp) targetTemp = savedTemp;
if (savedMode) mode = savedMode;
updateDisplay();
// Register command handler
window.KingKiosk.onCommand(function(command, payload) {
console.log('Command received:', command, payload);
switch (command) {
case 'set_temperature':
targetTemp = payload.temperature;
window.KingKiosk.storage.set('targetTemp', targetTemp);
updateDisplay();
sendTelemetry();
break;
case 'set_mode':
mode = payload.mode;
window.KingKiosk.storage.set('mode', mode);
updateDisplay();
sendTelemetry();
break;
case 'increment':
targetTemp++;
window.KingKiosk.storage.set('targetTemp', targetTemp);
updateDisplay();
sendTelemetry();
break;
case 'decrement':
targetTemp--;
window.KingKiosk.storage.set('targetTemp', targetTemp);
updateDisplay();
sendTelemetry();
break;
}
});
// Send initial telemetry
sendTelemetry();
// Periodic telemetry (every 30 seconds)
setInterval(sendTelemetry, 30000);
});
// Button handlers
btnUp.addEventListener('click', function() {
targetTemp++;
window.KingKiosk.storage.set('targetTemp', targetTemp);
updateDisplay();
sendTelemetry();
window.KingKiosk.sendCommand('temperature_changed', {
temperature: targetTemp,
direction: 'up'
});
});
btnDown.addEventListener('click', function() {
targetTemp--;
window.KingKiosk.storage.set('targetTemp', targetTemp);
updateDisplay();
sendTelemetry();
window.KingKiosk.sendCommand('temperature_changed', {
temperature: targetTemp,
direction: 'down'
});
});
</script>
</body>
</html>
Best Practices¶
1. Always Wait for the Bridge¶
// Good
window.addEventListener('kingkiosk-ready', function() {
window.KingKiosk.onCommand(...);
});
// Bad - bridge may not be ready
window.KingKiosk.onCommand(...);
2. Handle Missing Bridge Gracefully¶
function initWidget() {
if (window.KingKiosk) {
// Running in KingKiosk
setupBridgeHandlers();
} else {
// Running standalone (for testing)
console.log('Running without KingKiosk bridge');
setupMockData();
}
}
window.addEventListener('kingkiosk-ready', initWidget);
// Fallback for standalone testing
setTimeout(function() {
if (!window.KingKiosk) initWidget();
}, 1000);
3. Use Responsive Design¶
/* Support different window sizes */
body {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
/* Adapt to platform */
.tv-mode .text { font-size: 2em; }
.touch-mode .button { min-height: 44px; }
4. Minimize Network Requests¶
// Cache data locally
let cache = {};
async function getData(key) {
if (!cache[key]) {
cache[key] = await fetchFromApi(key);
}
return cache[key];
}
5. Use Throttled Telemetry¶
// Don't spam telemetry
let telemetryTimer = null;
function queueTelemetry(data) {
if (telemetryTimer) clearTimeout(telemetryTimer);
telemetryTimer = setTimeout(function() {
window.KingKiosk.publishTelemetry(data);
}, 1000);
}
6. Persist Important State¶
// Save state on every change
function updateTemperature(newTemp) {
temperature = newTemp;
window.KingKiosk.storage.set('temperature', temperature);
updateDisplay();
}
Troubleshooting¶
Widget Shows "Not Configured"¶
- Ensure you provided
url,html, orhtml_base64 - Check that URL is accessible (CORS may block some URLs)
Bridge Not Available¶
- Wait for
kingkiosk-readyevent - Check browser console for errors
- Verify the widget is loaded in KingKiosk (not standalone browser)
Commands Not Received¶
- Local
customWebView: verify topickingkiosk/{device_id}/window/{window_id}/commandand payload includes"action" - Remote Browser bridge: verify topic
kingkiosk/{device_id}/element/{remote_browser_window_id}/cmdand payload uses"command": "widget_command" - Ensure widget registered handler with
onCommand()
Telemetry Not Publishing¶
- Check MQTT connection status
- Verify device name is set
- Look for errors in KingKiosk logs
Storage Not Persisting¶
- Local
customWebView: storage is runtime-scoped to the active tile/controller - Remote Browser bridge: storage persists via app storage (
custom_widget_bridge_storage_v1:*) - Verify you're calling
storage.set()correctly - Check that widget ID hasn't changed
Debug Tips¶
// Enable verbose logging
window.addEventListener('kingkiosk-ready', async function() {
console.log('Bridge ready, widget ID:',
(await window.KingKiosk.getWidgetInfo()).widgetId);
window.KingKiosk.onCommand(function(cmd, payload) {
console.log('[CMD]', cmd, JSON.stringify(payload));
});
});
// Monitor all storage
setInterval(async function() {
const all = await window.KingKiosk.storage.getAll();
console.log('[STORAGE]', all);
}, 5000);
Platform Support¶
| Platform | Local customWebView |
Remote Browser custom widget bridge | Notes |
|---|---|---|---|
| macOS | Yes | Yes | Either path works. |
| iOS | Yes | Yes | Either path works. |
| tvOS | No | Yes | tvOS uses Remote Browser path. |
| Android | Yes | Yes | Either path works. |
| Windows | Yes | Yes | Either path works. |
| Linux | Yes | Yes | Either path works. |
| Web | Yes | Depends on WebRTC/bridge support | Verify in your target browser/runtime. |
tvOS Special Requirements¶
tvOS has no local customWebView tile runtime - it uses Remote Browser sessions.
Use a remote browser command (same command surface used on other platforms when desired):
{
"command": "create_remote_browser",
"window_id": "my_widget_tv",
"name": "My Widget",
"initial_url": "https://example.com/widget/",
"auto_connect": true
}
server_url is optional; if omitted, the app uses configured Feature Server settings.
On tvOS (Remote Browser path), the custom widget bridge supports:
- KingKiosk.onCommand()
- KingKiosk.sendCommand()
- KingKiosk.publishTelemetry()
- KingKiosk.storage.get/set/getAll()
- KingKiosk.getWidgetInfo()
For MQTT commands into widgets on tvOS, use the element command topic with command: "widget_command" (see sections above).
Need Help?¶
- Check the MQTT Widget Reference for more details
- Review the example widgets directory
- Open an issue on GitHub for bugs or feature requests