ZeroTrace Desktop
Device API
Calling the connected device from an app, handling results and errors, and the device.on event model
The device object represents the hardware you have connected — or the built-in simulator, when nothing is plugged in. You use it to ask the device to perform an operation and get the result back, and to subscribe to events the device pushes while it runs.
This page covers how you call the device from your script. What each operation actually does — the fields a result carries, the exact behavior of a scan or a capture — lives on that device's own capability pages, linked below. The App Builder also suggests the operations the connected (or simulated) device supports as you type, so you can discover them without leaving the editor.
Calling pattern
Every device operation is called the same way, however it's named: device.<namespace>.<method>(params?, timeoutMs?), or the equivalent flattened form device.rpc("<namespace>.<method>", params?, timeoutMs?). Both reach the same operation — pick whichever reads better for the call you're making.
// Dotted form
const s = await device.state.read()
// Equivalent flattened form
const s = await device.rpc("state.read")
// With params and a timeout
await device.led.set({ r: 255, g: 0, b: 0 }, 5000)
Every call returns a Promise, so use await inside an async handler (a button's onClick, a control's onChange, a ui.every timer). Nothing about the shape of a call tells you whether the device is actually connected — calling into a disconnected device rejects the promise the same way a failed operation would, so the error-handling pattern below covers both.
Handling results and errors
Wrap a call in try/catch (or .catch(...)) and report failures to the Console rather than letting them disappear:
ui.button({
label: "Read state",
onClick: async () => {
try {
const s = await device.rpc("state.read")
console.log("op:", s.op, "battery:", s.battery_pct + "%")
} catch (e) {
console.error("state.read failed:", e.message || e)
}
},
})
You don't strictly have to catch every call yourself — an error thrown inside a button's onClick, a control's onChange, a ui.every/ui.after timer, or a device.on callback is caught by the runtime and reported to the Console regardless, including a rejected promise from an async handler. Catching explicitly is still worth doing when you want a specific, readable message instead of a raw error object, or want the rest of the handler to keep running after a failure.
A result's fields depend on which device answered — for example a state read comes back with op/free_heap on one device family and mode/heap_free on another. Read defensively with ?? when a script needs to run against more than one device family: s.free_heap ?? s.heap_free.
Subscribing to events (device.on)
device.on(event, callback) subscribes to a stream of events the device pushes on its own — ongoing activity, not the result of a specific call. It returns a handle with .off() to unsubscribe early; otherwise every subscription from the current run is cleared automatically before the next Run, the same way timers are.
const sub = device.on("scan.hit", (hit) => {
console.log("saw", hit.mac, hit.rssi)
})
// Later, if you want to stop early:
// sub.off()
device.on(...) always lives at the top level of device — never on a namespaced path like device.scan.on(...). Writing it on a namespace is treated as a call to an operation literally named "scan.on" with your callback as a parameter, which is never what you meant. The runtime recognizes this specific shape (a string then a function) and throws a clear TypeError pointing at device.on(...) instead of failing silently.
What operations a device offers
The operations available depend on which device is connected — an Echo exposes sub-GHz operations; an AirLeak Pro exposes capture, radio, and location operations. The exact operation set, and what each one's result contains, is documented per device rather than duplicated here:
Wi-Fi capture, BLE capture, GPS wardriving, and logging operations.
Sub-GHz capture, decoders, rolling code, and RF counter-surveillance operations.
Running without hardware
When no device is connected, the App Builder can run your app against a built-in simulator instead — it answers the same calls and can emit the same events, so you can build and test the shape of an app before you're at your bench. Simulated and real devices are called exactly the same way from your script; nothing in the code above needs to change to point at real hardware later.