app/rcc/ai/skills/output_widgets.md
Output widgets (Pro) are interactive controls that transmit bytes back to the device. Each one belongs to a group and has its own JavaScript "transmit function" that converts UI state into bytes.
Output transmit functions are JavaScript-only — the Lua-first
guidance in the frame_parsers and transforms skills does NOT apply
here. A small set of Modbus/CAN protocol helpers is JS-bound
(see The transmit function); author transmit
functions in JS, Lua will not compile.
Output widgets live in a group's output panel. Each has a type:
[minValue, maxValue] with stepSize and
initialValue. Sends on drag-end.project.outputWidget.add{groupId, type} // create
project.outputWidget.update{groupId, widgetId, title?, icon?,
transmitFunction?} // configure
project.outputWidget.get{groupId, widgetId} // read current config
Each widget has:
title, icon: UI labelsminValue, maxValue, stepSize, initialValue: numeric range
(sliders, knobs)transmitFunction: JS source that returns the bytes to sendThe function name is transmit (NOT output / send). It receives a
single scalar value — numeric for slider/knob, 1/0 for toggle, the
string for textfield, and 1 for a button press. There is no state
object. It returns either a string (encoded with the widget's TX encoding)
or a byte array.
The only injected protocol globals are modbusWriteRegister,
modbusWriteCoil, modbusWriteFloat, canSendFrame, and canSendValue.
NMEA, GRBL, GCode, SCPI, SLCAN, CRC, and binary-packet logic are NOT
globals — adapt a bundled reference script (next section) that self-contains
that code. Full details: meta.fetchScriptingDocs{kind: "output_widget_js"}.
// Simple: send a Modbus-shaped command on button click
function transmit() {
return ":01030000000AF1\r\n";
}
// Slider: send a 16-bit big-endian value
function transmit(value) {
const v = Math.round(value) & 0xFFFF;
return new Uint8Array([0xA5, (v >> 8) & 0xFF, v & 0xFF]);
}
// Toggle: send different payload per state
function transmit(checked) {
return checked ? "RELAY ON\n" : "RELAY OFF\n";
}
// TextField: prepend AT prefix
function transmit(text) {
return "AT+" + text + "\r\n";
}
project.outputWidget.get{groupId, widgetId} —
preserve user-set ranges/labels when rewriting transmitFunction.meta.fetchScriptingDocs{kind: "output_widget_js"}.scripts.list{kind: "output_widget_js"} ships
reference scripts for NMEA checksums, Modbus framing, SLCAN, GRBL,
GCode, SCPI, AT commands, JSON, and binary packets — each self-contains
its protocol logic in a transmit(value) you copy and tweak.assistant.script.dryRun{kind: "output_widget", code, inputValue?, hex?}.
It compiles transmit() in the real protocol-helper + table-API
environment and returns {ok, compileError?, line?}. Pass inputValue
(and hex: true for hex byte input) to also run transmit() once and
get sampleRun.outputHex + byteCount — the only way to catch runtime
errors and wrong byte output before the widget ships.project.outputWidget.update{groupId, widgetId, transmitFunction: "..."}.Output widgets render in the OutputPanel widget. To pin a panel to a workspace:
project.workspace.setCustomizeMode{enabled: true}
project.workspace.addWidget{workspaceId, widgetType: "output-panel",
groupId}
// integer form (back-compat): widgetType: 18
// relativeIndex defaults to next free slot when omitted
The OutputPanel shows ALL output widgets in that group on one tile. Don't add one tile per output widget — that's not how it's structured.
You cannot transmit bytes yourself. console.send, io.writeData,
io.connect, and io.disconnect are deviceGated at the dispatcher:
blocked while the user's "Allow device control" toggle is off; with it
on they resolve to AlwaysConfirm, so each call still needs explicit
approval. Tools that hit those paths while blocked return
category: "hardware_write_blocked"
(distinct from permission_denied, which is OS-level only).
The whole point of an output widget is that the user presses the button and the JS transmit function you generated runs on their behalf. Frame your help around that: write a clean transmit() that produces the right bytes, and let the user be the one who fires it.
Output widget transmit functions and Painter scripts are JavaScript-only on purpose:
Frame parsers and per-dataset transforms support BOTH Lua (preferred for hotpath performance) and JavaScript. The Lua nudge does NOT extend to output widgets or painters.