Skip to main content

GPIO command sequences

A GPIO command sequence sends a list of I/O commands to the AT1000 in a single request. The device executes them back to back, holding the I/O subsystem for the whole run so that no other I/O request can interleave, and answers with a single report describing what every command did.

Use a sequence when the timing between reads and writes matters, when you need to wait for a signal to reach a logic level or a voltage band, or when you want higher-throughput acquisition without paying one network round-trip per operation. Every pin a sequence touches must already be configured with configure_input() or configure_output() before you call run(): a sequence drives and reads pins, it never changes their mode or direction. GPIO command sequences are available since AT1000 firmware 0.6.0, with the NodeJS SDK 0.7.0, the Python SDK 0.4.0 and the Rust SDK 0.2.0.

Commands​

CommandWhat it doesParameters
readReads one pin, or a list of pins that it walks in the order given. It can repeat the whole list and assert on what it measures.io (one pin or a list of pins); optionally repeat (number of passes over the list, at least 1), delay (gap between two passes, needs repeat of 2 or more) and expect
writeDrives one pin, or a list of pins that it walks in the order given.io (one pin or a list of pins) and value: a single value applied to every listed pin, or one value per pin, in which case the list of values is exactly as long as the list of pins
wait_voltageWaits for an analog pin to read at or above min, at or below max, or inside the inclusive band both of them describe.io (a single pin), at least one of min and max; optionally timeout and stable_for
wait_levelWaits for a digital pin to read low or high.io (a single pin) and level; optionally timeout and stable_for
delayWaits on the device without touching any pin.duration
holdHolds the outputs, so that every later write is buffered instead of being driven.none
releaseLatches every buffered write onto the pins at once.none

Durations, that is delay, duration, timeout and stable_for, are in seconds.

expect is checked against every reading a read takes: a boolean on a digital pin, a min/max band in volts on an analog pin. A reading that does not satisfy it stops the sequence.

Both waits are level-based and not edge-based: a pin that already satisfies the condition when the command starts completes immediately. stable_for requires the condition to hold across samples spanning that long.

hold and release are the output hold mechanism of gpio.hold() and gpio.release(), described in Output synchronization. They are independent from the exclusive access that the sequence itself takes for the duration of the run.

Building and running a sequence​

The example below configures pins 0, 1 and 2 as digital outputs and pin 3 as an analog output, then runs a ten-command sequence in a single request.

import { AT1000 } from '@ikalogic/at1000';

const pins = [0, 1, 2];

const devices = await AT1000.findDevices();
if (devices.length === 0) {
throw new Error('No devices found');
}

const tester = await AT1000.open(devices[0], { label: 'gpio_sequence' });
for (const io of pins) {
await tester.gpio.digital(io).configure_output({ value: false, vol: 0, voh: 5, vil: 1, vih: 4 });
}
await tester.gpio.analog(3).configure_output(2.5);

const report = await tester.gpio.sequence()
.hold()
.write(pins, [true, true, false])
.release()
.read(pins).as('scan')
.wait_voltage(3, { min: 2, max: 3, timeout: 0.5 }).as('band')
.wait_level(0, 'high', { stable_for: 0.02, timeout: 0.5 }).as('ready')
.write(0, false)
.read(0, { repeat: 3, delay: 0.005, expect: false }).as('burst')
.delay(0.01).as('pause')
.read(1, { expect: true }).as('final')
.run();

console.log(report.named.scan.samples); // One sample per pin
console.log(report.named.band.value); // The voltage that satisfied the wait
console.log(report.named.burst.samples.length); // 3 readings
console.log(`${(report.elapsed * 1000).toFixed(1)} ms on the device`);

The sequence above, step by step:

  1. hold() holds the outputs. Every write that follows is buffered instead of being driven onto the pins.
  2. write([0, 1, 2], [true, true, false]) drives pins 0, 1 and 2 in the order given, one value per pin. Because the outputs are held, nothing changes on the pins yet.
  3. release() latches the three buffered values at once, so pins 0, 1 and 2 change together instead of one after the other.
  4. read([0, 1, 2]), named scan, reads the three pins back. A pin list makes this a collection result: samples holds one entry per pin, each with its own elapsed timestamp.
  5. wait_voltage(3, { min: 2, max: 3, timeout: 0.5 }), named band, waits for analog pin 3 to read inside the 2 V to 3 V band and gives up after 500 ms. Pin 3 was configured to output 2.5 V, so the condition already holds and the command returns at once with the reading that satisfied it.
  6. wait_level(0, 'high', { stable_for: 0.02, timeout: 0.5 }), named ready, waits for digital pin 0 to read high and to stay high for 20 ms. Any sample that reads low restarts those 20 ms from scratch.
  7. write(0, false) drives pin 0 low. The outputs are no longer held, so the pin changes immediately. A single-pin write is a scalar result carrying the value that was actually applied.
  8. read(0, { repeat: 3, delay: 0.005, expect: false }), named burst, reads pin 0 three times with 5 ms between two passes, and asserts that every reading is low. A repeat above 1 makes this a collection result as well.
  9. delay(0.01), named pause, waits 10 ms on the device. It is a control result: it reports its elapsed and nothing else.
  10. read(1, { expect: true }), named final, takes one reading on pin 1 and asserts that it is high.

Only run() talks to the device. Appending and naming a command is a local operation that validates its arguments, and the ten commands travel to the device in a single request.

Named results​

.as('name') in NodeJS, and .as_("name") in Python and Rust, tags the command that was just appended. A name must be nonempty, must be unique within the sequence, and a command carries at most one of them, so naming twice in a row or naming before any command has been appended is an error. Names never reach the device: there is no name field on the wire, they are a client-side index into the report.

A named result comes in one of three kinds:

  • scalar for a single-pin read, a single-pin write and both waits. It carries a single value: the reading for a read, the value that was actually applied for a write, and the reading that satisfied the wait for wait_voltage and wait_level.
  • collection for a read over a pin list or with a repeat above 1, and for a multi-pin write. It carries samples, a list of { io, value, elapsed } entries in acquisition order, one per pin and per pass.
  • control for hold, release and delay. It carries elapsed only.

How you reach them depends on the SDK:

  • NodeJS: report.named.scan, typed per name at compile time, so the type checker already knows that report.named.scan.samples and report.named.band.value exist. Use report.get('scan') for a name that is only known at runtime.
  • Python: report.named["scan"], a union discriminated on .kind, which is "scalar", "collection" or "control". Narrow it with isinstance() or by testing .kind.
  • Rust: report.get("scan") returns Option<SequenceNamedResult>, an enum with a Scalar, a Collection and a Control variant to match on.

Alongside the named results, report.results (report.results() in Rust) is the flat list of every step, named or not, with exactly one step per submitted command and in the order the commands were sent. Every elapsed, on the report, on a step and on a sample, is a number of seconds counted from the start of the run and measured on the device, not an HTTP round-trip time.

Failures​

A failed sequence still leaves the device where it stopped

A run that does not complete returns an error and never a partial report:

  • SEQUENCE_TIMED_OUT, HTTP 409: a wait exceeded its own timeout, or the sequence exhausted its global timeout, which is 30 s by default.
  • SEQUENCE_ASSERTION_FAILED, HTTP 409: a read took a reading that its expect rejected.

The message names the command index, its op and the value observed, for instance Command 4 ('wait_voltage'): pin 3 still reads 2.1000 V. It surfaces as an ApiError carrying that code in NodeJS and in Python, and as At1000Error::Api(error) with error.code in Rust.

Writes that already executed stay applied, and a hold that was not followed by its release leaves the outputs held. Nothing is rolled back on the hardware, so call gpio.release() in your error path.

Validation errors, such as an unconfigured pin, an out-of-range voltage, an unknown field or an empty list, are HTTP 400 instead: the whole sequence is rejected before anything runs, and the device is left untouched.

Limits​

  • A sequence carries from 1 to 65536 commands. An empty list is rejected.
  • The global timeout defaults to 30 s and bounds the whole run, including every pass of a repeated read. The timeout of a wait is clamped to what is left of that budget, so a wait never outlives the sequence.
  • Pins are numbered 0 to 31. read and write take a single pin or a list of distinct pins, wait_voltage and wait_level take a single pin.
  • A write accepts a logic level on a digital pin, and a voltage between 0 V and 24 V on an analog pin.
  • expect bounds, min and max must sit within [-25, 25] V.
  • Levels and voltages come from polled ADC samples, so a pulse shorter than one sampling interval is not observable by wait_level or wait_voltage.
  • stable_for restarts from scratch whenever a sample fails the condition, so a noisy signal makes a wait last longer than stable_for.

See the GPIO API Reference for every method and type.