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​
| Command | What it does | Parameters |
|---|---|---|
read | Reads 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 |
write | Drives 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_voltage | Waits 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_level | Waits for a digital pin to read low or high. | io (a single pin) and level; optionally timeout and stable_for |
delay | Waits on the device without touching any pin. | duration |
hold | Holds the outputs, so that every later write is buffered instead of being driven. | none |
release | Latches 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.
- NodeJS
- Python
- Rust
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`);
from ikalogic_at1000 import AT1000
pins = [0, 1, 2]
devices = AT1000.find_devices()
if not devices:
raise RuntimeError("No devices found")
tester = AT1000.open(devices[0], label="gpio_sequence")
for io in pins:
tester.gpio.digital(io).configure_output(value=False, vol=0.0, voh=5.0, vil=1.0, vih=4.0)
tester.gpio.analog(3).configure_output(2.5)
sequence = (
tester.gpio.sequence()
.hold()
.write(pins, [True, True, False])
.release()
.read(pins).as_("scan")
.wait_voltage(3, min=2.0, max=3.0, 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")
)
report = sequence.run(timeout=10.0)
print(report.named["scan"].samples) # One sample per pin
print(report.named["band"].value) # The voltage that satisfied the wait
print(len(report.named["burst"].samples)) # 3 readings
print(f"{report.elapsed * 1000:.1f} ms on the device")
use ikalogic_at1000::{AT1000, DigitalOutputConfig, GpioValue, OpenOptions, ReadOptions, SequenceExpect, SequenceNamedResult, WaitLevelOptions, WaitVoltageOptions};
use std::time::Duration;
let devices = AT1000::find_devices(Duration::from_millis(500))?;
let tester = AT1000::open_with(&devices[0], OpenOptions { label: Some("gpio_sequence".into()), ..Default::default() })?;
for io in [0, 1, 2] {
tester.gpio.digital(io)?.configure_output(&DigitalOutputConfig { value: false, vol: 0.0, voh: 5.0, vil: Some(1.0), vih: Some(4.0) })?;
}
tester.gpio.analog(3)?.configure_output(2.5)?;
let report = tester.gpio.sequence()
.hold()?
.write(vec![0, 1, 2], vec![GpioValue::Digital(true), GpioValue::Digital(true), GpioValue::Digital(false)])?
.release()?
.read(vec![0, 1, 2], ReadOptions::default())?.as_("scan")?
.wait_voltage(3, WaitVoltageOptions { min: Some(2.0), max: Some(3.0), timeout: Some(0.5), ..Default::default() })?.as_("band")?
.wait_level(0, true, WaitLevelOptions { stable_for: Some(0.02), timeout: Some(0.5) })?.as_("ready")?
.write(0, GpioValue::Digital(false))?
.read(0, ReadOptions { repeat: Some(3), delay: Some(0.005), expect: Some(SequenceExpect::Level(false)) })?.as_("burst")?
.delay(0.01)?.as_("pause")?
.read(1, ReadOptions { expect: Some(SequenceExpect::Level(true)), ..Default::default() })?.as_("final")?
.run(None)?;
println!("{:?}", report.get("scan")); // A collection result, one sample per pin
println!("{:?}", report.get("band")); // A scalar result holding the voltage
if let Some(SequenceNamedResult::Collection { samples, .. }) = report.get("burst") {
println!("{} readings in the burst", samples.as_slice().len()); // 3 readings
}
println!("{:.1} ms on the device", report.elapsed * 1000.0);
The sequence above, step by step:
hold()holds the outputs. Every write that follows is buffered instead of being driven onto the pins.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.release()latches the three buffered values at once, so pins 0, 1 and 2 change together instead of one after the other.read([0, 1, 2]), namedscan, reads the three pins back. A pin list makes this a collection result:samplesholds one entry per pin, each with its ownelapsedtimestamp.wait_voltage(3, { min: 2, max: 3, timeout: 0.5 }), namedband, 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.wait_level(0, 'high', { stable_for: 0.02, timeout: 0.5 }), namedready, 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.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.read(0, { repeat: 3, delay: 0.005, expect: false }), namedburst, reads pin 0 three times with 5 ms between two passes, and asserts that every reading is low. Arepeatabove 1 makes this a collection result as well.delay(0.01), namedpause, waits 10 ms on the device. It is a control result: it reports itselapsedand nothing else.read(1, { expect: true }), namedfinal, 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-pinwriteand both waits. It carries a singlevalue: the reading for a read, the value that was actually applied for a write, and the reading that satisfied the wait forwait_voltageandwait_level. - collection for a
readover a pin list or with arepeatabove 1, and for a multi-pinwrite. It carriessamples, a list of{ io, value, elapsed }entries in acquisition order, one per pin and per pass. - control for
hold,releaseanddelay. It carrieselapsedonly.
How you reach them depends on the SDK:
- NodeJS:
report.named.scan, typed per name at compile time, so the type checker already knows thatreport.named.scan.samplesandreport.named.band.valueexist. Usereport.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 withisinstance()or by testing.kind. - Rust:
report.get("scan")returnsOption<SequenceNamedResult>, an enum with aScalar, aCollectionand aControlvariant 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 run that does not complete returns an error and never a partial report:
SEQUENCE_TIMED_OUT, HTTP 409: a wait exceeded its owntimeout, or the sequence exhausted its globaltimeout, which is 30 s by default.SEQUENCE_ASSERTION_FAILED, HTTP 409: areadtook a reading that itsexpectrejected.
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
timeoutdefaults to 30 s and bounds the whole run, including every pass of a repeatedread. Thetimeoutof a wait is clamped to what is left of that budget, so a wait never outlives the sequence. - Pins are numbered 0 to 31.
readandwritetake a single pin or a list of distinct pins,wait_voltageandwait_leveltake a single pin. - A
writeaccepts a logic level on a digital pin, and a voltage between 0 V and 24 V on an analog pin. expectbounds,minandmaxmust 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_levelorwait_voltage. stable_forrestarts from scratch whenever a sample fails the condition, so a noisy signal makes a wait last longer thanstable_for.
See the GPIO API Reference for every method and type.