Polling Device State
A FrontPanel application is rarely just reacting to user input. Instrumentation, control, and test-and-measurement applications spend most of their monitoring telemetry — surfacing counter readings, status flags, instrument measurements, queue depths, captured frames. The device doesn’t push this information; the application asks for it on a schedule. That’s polling, and how you structure it shapes both the responsiveness of the UI and the load on the device / communication interface.
Polling looks like one technique, but two distinct patterns sit underneath it:
- Telemetry — periodic reads of device state for display. Small, cheap, frequent. A handful of registers refreshed several times per second.
- Acquisition — event-driven capture of a larger payload. Heavier, multi-step, and gated on the device being ready. A trigger, a wait, and a bulk read.
The cadence, cost, and lifecycle of each are different enough that they’re worth treating separately. The building blocks are the same.
Building Blocks
FrontPanel applications implement polling using standard React and JavaScript primitives, plus the WorkQueue from @opalkelly/frontpanel-platform-api:
setInterval(callback, ms)— firescallbackeverymsmilliseconds. Returns a handle you’ll need in order to cancel the timer later.React.useEffect(setup, deps)— runssetupwhen the component mounts, and again whenever any value indepschanges. The function returned bysetupruns on cleanup. This is where you start and stop the timer so polling lifetime tracks the component.workQueue.post(asyncTaskFn)— addsasyncTaskFnto a serial queue. Wrapping device calls inworkQueue.post()ensures only one device transaction is in flight at a time. See Work Queue for the full rationale.
The general shape of a poll, then, is: a useEffect that starts a setInterval, where each tick posts a device transaction to the work queue, and whose cleanup function clears the interval when the component unmounts.
Telemetry
Telemetry polls are small and frequent. A device handles roughly 1 kHz of round-trips for small operations like wire reads, so a telemetry poll costs about one millisecond of device time per cycle. (See the performance reference for details.) The budget is generous but not unlimited — group your reads sensibly to stay well under it.
updateWireOuts() fetches all 32 wire-out registers in a single round-trip and caches them on the host. getWireOutValue() reads from that cache. Call updateWireOuts() once per poll cycle and read every value you need from the cache — there is no per-wire cost beyond the initial fetch.
A common grouping is by instant: values that must reflect the same moment — voltage, current, and temperature on a power dashboard — should share a poll so they’re sampled together.
function PowerDashboard(props: { fpgaDataPort: IFPGADataPortClassic; workQueue: WorkQueue }) {
const [voltage, setVoltage] = React.useState(0);
const [current, setCurrent] = React.useState(0);
const [temperature, setTemperature] = React.useState(0);
React.useEffect(() => {
const id = setInterval(() => {
props.workQueue.post(async () => {
await props.fpgaDataPort.updateWireOuts();
setVoltage(props.fpgaDataPort.getWireOutValue(0x20));
setCurrent(props.fpgaDataPort.getWireOutValue(0x21));
setTemperature(props.fpgaDataPort.getWireOutValue(0x22));
});
}, 100);
return () => clearInterval(id);
}, [props.fpgaDataPort, props.workQueue]);
return (
<>
<Gauge label="Voltage" value={voltage} />
<Gauge label="Current" value={current} />
<Gauge label="Temperature" value={temperature} />
</>
);
}Code language: TypeScript (typescript)One updateWireOuts() per tick, three values pulled from the cache, all sampled at the same instant.
Acquisition
Acquisition is a different shape. Where telemetry is a single read, an acquisition cycle is a transaction: trigger the device, wait for it to signal ready, then read the captured data. Each step is a round-trip, and the read at the end can be much larger than a status poll. A typical cycle is roughly six round-trips — one to trigger, several waiting on a status flag, one (longer) to transfer the data — versus one for a telemetry poll.
The implication is that acquisition cycles are scheduled less aggressively than telemetry, and the cadence is set by how long the device takes to produce and transport the data, not by UI refresh expectations.
function CameraView(props: { fpgaDataPort: IFPGADataPortClassic; workQueue: WorkQueue }) { const canvasRef = React.useRef<HTMLCanvasElement>(null); React.useEffect(() => { const id = setInterval(() => { props.workQueue.post(async () => { // Trigger acquisition await props.fpgaDataPort.activateTriggerIn(0x40, 1); // Poll status until the frame-ready flag is set while (true) { await props.fpgaDataPort.updateWireOuts(); if (props.fpgaDataPort.getWireOutValue(0x60) & 0x1) break; } // Read frame data const frame = new Uint8Array(FRAME_SIZE); await props.fpgaDataPort.readFromPipeOut(0xA0, frame); if (canvasRef.current) drawFrame(canvasRef.current, frame); }); }, 33); return () => clearInterval(id); }, [props.fpgaDataPort, props.workQueue]); return <canvas ref={canvasRef} />; }Code language: TypeScript (typescript)In this case, the entire trigger / wait / read sequence is one work queue ticket. Posting it as a single ticket is what keeps the steps from interleaving with other device traffic — a telemetry poll from another component can’t slip in between the trigger and the read.
If your gateware and application design can accommodate the response latency, you may prefer to break the cycle into separate work queue tickets to avoid blocking telemetry. Incorporating the wait check into telemetry is also an option.
Cleanup and Unmount
The function returned from
useEffectruns when the component unmounts and before the effect re-runs because a dependency changed. Always callclearIntervalthere. Two things go wrong if you forget: the interval keeps firing after the component is gone, leaking device transactions indefinitely, and React warns about state updates on an unmounted component.React.useEffect(() => { const id = setInterval(/* ... */); return () => clearInterval(id); // ← essential }, [/* deps */]);Code language: TypeScript (typescript)This applies equally to telemetry and acquisition. Any
setIntervalstarted in auseEffectneeds a matchingclearIntervalin the cleanup.