Command sequences
A command sequence is a list of steps that the AT1000 runs on its own, one after the other, from a single request. A step can switch a relay, power the DUT, drive or read a pin, wait for a signal, measure a PWM clock, wait for a USB device, or write to the screen. The device sends back a single report at the end, with the result of every step.
Use a sequence when:
- the timing between steps matters. The device starts each step as soon as the previous one ends, with no network delay between them.
- you need to wait for something on the device side, such as a pin going high, a supply reaching its voltage or a USB device showing up.
- you want to check values as they are measured, for instance that the DUT never draws more than 200 mA, and stop at the first bad reading.
- you run many short operations and want to avoid one network round trip for each of them.
Command sequences need AT1000 firmware 0.13.0 or later, and one of these SDKs:
- NodeJS SDK 0.13.0 or later
- Python SDK 0.11.0 or later
- Rust SDK 0.8.0 or later
A first sequence​
The sequence below powers the DUT at 5 V, waits until the supply is within 4.8 V to 5.2 V, then waits until the DUT pulls its "ready" line (pin 1) high.
- NodeJS
- Python
- Rust
import { AT1000 } from '@ikalogic/at1000';
const devices = await AT1000.findDevices();
const tester = await AT1000.open(devices[0]);
const report = await tester.sequence()
.power.dut.enable(0, 5)
.power.dut.wait(0, { voltage: { min: 4.8, max: 5.2 }, timeout: 1 })
.gpio.configure(1, { mode: 'digital', direction: 'input', vil: 0.8, vih: 2 })
.gpio.wait_level(1, 'high', { timeout: 0.5 }).as('ready')
.run();
console.log(`The DUT was ready after ${(report.named.ready.elapsed * 1000).toFixed(1)} ms`);
from ikalogic_at1000 import AT1000
devices = AT1000.find_devices()
tester = AT1000.open(devices[0])
report = (
tester.sequence()
.power.dut.enable(0, 5.0)
.power.dut.wait(0, voltage=(4.8, 5.2), timeout=1.0)
.gpio.configure(1, mode="digital", direction="input", vil=0.8, vih=2.0)
.gpio.wait_level(1, "high", timeout=0.5).as_("ready")
.run()
)
print(f"The DUT was ready after {report.named['ready'].elapsed * 1000:.1f} ms")
use ikalogic_at1000::{At1000, GpioConfiguration, PowerBounds, WaitOptions};
use std::time::Duration;
let devices = At1000::find_devices(Duration::from_millis(500))?;
let tester = At1000::open(&devices[0], None)?;
let supply = PowerBounds { voltage: Some((4.8..=5.2).into()), ..Default::default() };
let report = tester.sequence()
.power().dut().enable(0, 5.0)
.power().dut().wait(0, supply, WaitOptions { timeout: Some(Duration::from_secs(1)), ..Default::default() })
.gpio().configure(1, GpioConfiguration::DigitalInput { vil: 0.8, vih: 2.0 })
.gpio().wait_level(1, true, WaitOptions { timeout: Some(Duration::from_millis(500)), ..Default::default() })
.named("ready")
.run(None)?;
if let Some(ready) = report.get("ready") {
println!("The DUT was ready after {:?}", ready.step.elapsed());
}
Building and running a sequence​
You build a sequence by adding steps to tester.sequence(), then call run(). Steps are grouped by what they control: gpio, gpio.pwm, relays, power.dut, power.usb, power.relay, hmi.screen, hmi.audio and com.usb. delay() belongs to no group: you call it on the sequence itself.
- Nothing happens until
run(). Adding a step only records it.run()sends every step to the device in one request and returns when the whole sequence is done. - Steps run in the order you add them. Each step starts when the previous one ends.
- A sequence can be reused. Adding a step gives you a new sequence and leaves the original as it was. You can run the same sequence several times, or keep a common start and add different endings to it.
run()has a time limit. The whole sequence must finish withintimeout, 30 s by default:run({ timeout: 10 })in NodeJS,run(timeout=10.0)in Python,run(Duration::from_secs(10))in Rust (run(None)keeps the default).- Times are in seconds in NodeJS and Python, and
Durationvalues in Rust. Voltages are in volts, currents in amperes, frequencies in hertz.
In Python, ikalogic_at1000.aio offers the same sequence for asyncio: you build it the same way and await its run().
In Rust, every step method returns the whole sequence, so you call the group method again before each step: .gpio().write(0, true).gpio().read(1, None). Optional settings take a value or None. A step with a wrong argument does not stop the chain: run() reports it, with the step number, before anything is sent.
What a sequence can do​
The tables below explains every step. The Sequence API Reference gives the parameters available for each one.
Timing
| Step | What it does |
|---|---|
delay() | Waits for a fixed time without touching anything. |
GPIO (details)
| Step | What it does |
|---|---|
gpio.configure() | Sets a pin's mode (digital or analog), direction and levels, like configure_input() and configure_output(). |
gpio.write() | Drives one pin, or several pins one after the other: a logic level on a digital pin, a voltage on an analog pin. |
gpio.read() | Reads one pin or several pins. It can read them several times in a row and check every reading. |
gpio.wait_level() | Waits until a digital pin reads low or high. |
gpio.wait_voltage() | Waits until an analog pin reads inside a voltage range. |
gpio.hold() | Holds the outputs: later writes are stored instead of being applied (see Output synchronization). |
gpio.release() | Applies every stored write at once. |
gpio.reset() | Resets one pin, or every pin. Resetting every pin also releases held outputs and turns PWM off. |
Every pin a step uses must be configured for that use when the step runs: either before run(), or by an earlier gpio.configure() step in the same sequence.
PWM (details)
| Step | What it does |
|---|---|
gpio.pwm.output() | Starts a square wave on IO 18 or IO 22, for a set time or until it is turned off. |
gpio.pwm.input() | Turns on PWM measurement on IO 17 or IO 21. |
gpio.pwm.measure() | Measures the frequency and duty cycle on IO 17 or IO 21, and can check them. |
gpio.pwm.disable() | Turns PWM off on one of these four pins. |
IO 17 and IO 18 share one channel, and so do IO 21 and IO 22: only one pin of each pair can use PWM at a time. To measure an AT1000 PWM output on its own PWM input (a loopback), wire IO 18 to IO 21, or IO 22 to IO 17.
Relays (details)
| Step | What it does |
|---|---|
relays.close() | Closes one relay, several relays, or all of them. |
relays.open() | Opens one relay, several relays, or all of them. |
relays.reset() | Opens every relay. |
Power (details)
| Step | What it does |
|---|---|
power.dut.enable() | Turns the DUT supply on at a target voltage (1.6 V to 13 V, or 24 V). |
power.dut.disable() | Turns the DUT supply off. |
power.dut.read() | Measures the DUT supply voltage and current, once or several times, and can check them. |
power.dut.wait() | Waits until the DUT supply voltage or current is inside a range. |
power.usb.enable() / disable() | Turns the power of a USB port on or off. |
power.usb.read() / wait() | Like the DUT steps, for the power of a USB port. |
power.relay.read() / wait() | Measures, or waits on, the current flowing through relay 0. These steps do not switch the relay. |
power.dut.reset(), power.usb.reset(), power.reset() | Turn the DUT supply, a USB port, or both supplies off. |
Screen and sound (details)
| Step | What it does |
|---|---|
hmi.screen.print() | Shows text on the AT1000 screen. |
hmi.screen.progress() | Shows a progress bar, from 0 to 100 %. 0 hides it. |
hmi.screen.colors() | Changes the text, progress bar or background color. |
hmi.screen.clear_text() | Clears the text. |
hmi.screen.clear() | Clears the text and the progress bar. |
hmi.audio.play() | Starts a sound. The sequence moves on without waiting for it to end. |
hmi.reset() | Resets the screen and the speaker. |
USB devices (details)
| Step | What it does |
|---|---|
com.usb.wait() | Waits until a USB device is connected to a port, or until none is. It can look for a vendor and product ID. |
Waiting and checking​
Waits​
The wait steps (gpio.wait_level(), gpio.wait_voltage(), power.dut.wait(), power.usb.wait(), power.relay.wait() and com.usb.wait()) keep measuring until their condition is met, then let the sequence go on. They take two optional settings:
timeout: how long the wait may last. If the condition is still not met after that time, the sequence stops with an error. Without atimeout, the wait may use whatever is left of the sequence's own time limit.stable_for: how long the condition must hold without interruption before the wait ends. A single reading that breaks the condition restarts this time from zero. Use it to ignore bounces and short glitches.
A wait looks at the current state, not at a change: if the condition is already met when the wait starts, it ends at once. Pins are sampled, so a pulse shorter than the time between two samples can go unseen.
Checks​
The read steps (gpio.read(), power.dut.read(), power.usb.read(), power.relay.read()) and gpio.pwm.measure() accept an expect setting. The device compares every reading with it, and the first reading that does not match stops the sequence with an error.
- On a digital pin,
expectis a level:trueorfalse. - For a voltage, a current, a frequency or a duty cycle,
expectis a range. Give a minimum, a maximum, or both. In NodeJS, write{ min: 4.8, max: 5.2 }or{ max: 0.2 }. In Python, write a pair such as(4.8, 5.2), withNonefor an open side:(None, 0.2). In Rust, write a range:4.8..=5.2,..=0.2or4.8...
When to use each:
- Use a wait when something will happen but you do not know exactly when, such as a supply rising or a DUT booting.
- Use a check when the value must already be right at that point, such as the current drawn once the DUT is running.
Repeated readings​
A read step can take several readings in a row with repeat, the total number of readings, and delay, the time between two of them. For instance, power.dut.read(0, { repeat: 5, delay: 0.01 }) measures the DUT supply five times, 10 ms apart. With expect, each of the five readings is checked.
Reading the results​
Naming steps​
To find a step's result easily, give the step a name right after adding it: .as('supply') in NodeJS, .as_("supply") in Python, .named("supply") in Rust. A name must not be empty, must be unique in the sequence, and each step can have only one. Names stay on your computer; they are not sent to the device.
What a result contains​
A named result tells you what the step measured or did:
- a single value for a one-off reading, a wait or a single-pin write:
valueholds the reading, the value that ended the wait, or the value that was applied. For the DUT supply,valueholds bothvoltageandcurrent. - a list of samples for a read or write over several pins, or a read with
repeat:samplesholds one entry per pin and per reading, in the order they were taken. - the step's own data for the other steps: the applied configuration for
gpio.configure()and the PWM steps, themeasurementforgpio.pwm.measure(), the matchingdevicesforcom.usb.wait(), or nothing.
Every result also carries elapsed: the time from the start of the sequence to the end of that step, measured on the device. report.elapsed is the duration of the whole sequence. The time spent waiting for another sequence to finish is not counted.
How to reach the results in each SDK:
- NodeJS:
report.named.supply. The names are known to TypeScript, soreport.named.supply.value.voltageis type-checked.report.get('supply')works with a name chosen at run time. - Python:
report.named["supply"], orreport.get("supply"). Itskindis"scalar"(single value),"collection"(samples) or"control"(other steps). - Rust:
report.get("supply")returns the step's position and itsSequenceStep, an enum with one variant per step type. Usematchorif letto read it, or the shortcutsstep.elapsed(),step.samples(),step.value(),step.level()andstep.voltage()for GPIO steps.
report.results (report.results() in Rust) lists the result of every step, named or not, in the order of the steps.
While a sequence runs​
- The GPIO, the relays and the power supplies are reserved for the sequence, from its first step to its last, if it uses them. Other requests to them, from your program or from anybody else, are not rejected: the device holds them until the sequence ends, then runs them. If a sequence runs longer than the time such a request may wait, that request fails with a timeout on the computer that sent it.
- Screen, sound and USB steps share the device with other requests. Other requests to the screen, the speaker or the USB ports can run between two steps of the sequence.
- A second sequence that needs the same GPIO, relays or power waits for the first one to finish. Its time limit starts when it is sent, so the waiting counts against it.
- If another session takes over the device (see Device access control), the sequence stops before its next step, and
run()fails withACCESS_REVOKED. The step in progress finishes, and the steps already done are not undone.
When a sequence fails​
A sequence either runs to the end and returns its report, or fails with an error. There is no partial report.
Rejected before it starts. Mistakes such as an unknown pin, a voltage out of range, or a write to a pin configured as an input are found before the first step runs, and nothing is executed on the device. The SDK finds most of them: NodeJS and Python raise an error when you add the step, and Rust returns InvalidInput from run() without sending anything. The device finds the others and answers with an HTTP 400 error.
Stopped while running. The sequence stops at the step that failed, with an HTTP 409 error:
| Error code | Why the sequence stopped |
|---|---|
SEQUENCE_TIMED_OUT | A wait reached its timeout, or the sequence reached its own time limit. |
SEQUENCE_ASSERTION_FAILED | A reading did not match its expect. |
ACCESS_REVOKED | Another session took over the device. |
The message tells which step failed (counting from 0) and what was measured, for instance: Command 4 (`power.dut.wait`): DUT 0 still reads 0.0010 V, 0.0010 A. How the error reaches your program:
- NodeJS and Python: an
ApiErrorwhosecodeis one of the codes above. - Rust:
At1000Error::Api, withErrorCode::SequenceTimedOutorErrorCode::SequenceAssertionFailedin itscode, orAt1000Error::Revokedfor a takeover.
The steps that ran before the failure keep their effect: relays stay closed, the DUT stays powered, a PWM output keeps running, and outputs held by gpio.hold() stay held. Put the device back in a known state in your error handling, for instance with tester.reset().
Limits​
- A sequence holds from 1 to 65536 steps.
- The time limit of
run()is 30 s by default. It covers the whole sequence, including the time spent waiting for another sequence to finish. - A digital
expect,gpio.wait_level()andgpio.wait_voltage()need a pin configured in the matching mode, digital or analog.
The Sequence API Reference lists the limits of each step.
Migrating from gpio.sequence()​
Before firmware 0.13.0, sequences could only use the GPIO, through tester.gpio.sequence(). That builder is replaced by tester.sequence(), whose GPIO steps sit in the gpio group:
| Before | Now (NodeJS and Python) | Now (Rust) |
|---|---|---|
tester.gpio.sequence() | tester.sequence() | tester.sequence() |
.read(0), .write(0, true) | .gpio.read(0), .gpio.write(0, true) | .gpio().read(0, None), .gpio().write(0, true) |
.wait_level(…), .wait_voltage(…) | .gpio.wait_level(…), .gpio.wait_voltage(…) | .gpio().wait_level(…), .gpio().wait_voltage(…) |
.hold(), .release() | .gpio.hold(), .gpio.release() | .gpio().hold(), .gpio().release() |
.delay(…) | .delay(…) | .delay(…) |
Naming, run() and the report work as before, except that a step's op in the report now carries its group: gpio.read instead of read.
What to know when you update:
- Update the firmware and the SDK together. Firmware 0.13.0 no longer accepts the sequences of older SDKs, and the new SDKs need firmware 0.13.0 or later for every sequence.
tester.gpio.sequence()still works with the new SDKs and firmware, but it is deprecated. It prints a warning once per program run. Set the environment variableAT1000_NO_DEPRECATION_WARNINGS=1to hide the warning.
Full example: testing a DUT​
This example runs a short end-of-line test in a single sequence. The DUT has a reset input on IO 0, a "ready" output on IO 1, a 10 kHz clock output on IO 17, and a USB port wired to the AT1000's USB port 0. Relays 0 and 1 connect the DUT to its load.
The sequence:
- shows "Testing…" on the screen,
- closes relays 0 and 1 and turns the DUT supply on at 5 V,
- waits for the supply to reach 4.8 V to 5.2 V, then checks five times, 10 ms apart, that the DUT draws at most 200 mA,
- holds the DUT in reset, then releases it and waits for its "ready" line to stay high for 5 ms,
- measures the DUT's clock and checks that it is between 9.9 kHz and 10.1 kHz,
- powers USB port 0 and waits for the DUT to appear on it,
- shows "PASS" and plays the success sound.
If any of these fails, the sequence stops and the program resets the AT1000, which also turns the DUT off.
- NodeJS
- Python
- Rust
import { AT1000 } from '@ikalogic/at1000';
const devices = await AT1000.findDevices();
const tester = await AT1000.open(devices[0], { label: 'end-of-line' });
await tester.reset();
const test = tester.sequence()
.hmi.screen.print('Testing…')
.hmi.screen.progress(10)
// Connect the DUT and power it
.relays.close([0, 1])
.power.dut.enable(0, 5)
.power.dut.wait(0, { voltage: { min: 4.8, max: 5.2 }, timeout: 2 }).as('supply')
.power.dut.read(0, { repeat: 5, delay: 0.01, expect: { current: { max: 0.2 } } }).as('current')
.hmi.screen.progress(40)
// Hold the DUT in reset, release it, wait until it is ready
.gpio.configure(0, { mode: 'digital', direction: 'output', value: false, vol: 0, voh: 3.3, vil: 0.8, vih: 2 })
.gpio.configure(1, { mode: 'digital', direction: 'input', vil: 0.8, vih: 2 })
.delay(0.01)
.gpio.write(0, true)
.gpio.wait_level(1, 'high', { timeout: 1, stable_for: 0.005 }).as('ready')
.hmi.screen.progress(60)
// Check the DUT's clock
.gpio.pwm.input(17)
.gpio.pwm.measure(17, { expect: { frequency: { min: 9900, max: 10100 } } }).as('clock')
.gpio.pwm.disable(17)
.hmi.screen.progress(80)
// Check that the DUT shows up on USB
.power.usb.enable(0)
.com.usb.wait(0, { connected: true, timeout: 5 }).as('usb')
.hmi.screen.progress(100)
.hmi.screen.print('PASS')
.hmi.audio.play('success', 50);
try {
const report = await test.run({ timeout: 15 });
const { supply, current, ready, clock, usb } = report.named;
console.log(`Supply: ${supply.value.voltage.toFixed(3)} V`);
for (const sample of current.samples) {
console.log(`Current: ${(sample.current * 1000).toFixed(1)} mA`);
}
console.log(`Ready ${(ready.elapsed * 1000).toFixed(1)} ms after the start`);
if (clock.measurement.status === 'ok') {
console.log(`Clock: ${clock.measurement.frequency.toFixed(0)} Hz`);
}
console.log(`USB: ${usb.devices.map((device) => device.product).join(', ')}`);
console.log(`Whole test: ${(report.elapsed * 1000).toFixed(1)} ms`);
} catch (error) {
await tester.reset();
throw error;
}
from ikalogic_at1000 import AT1000
devices = AT1000.find_devices()
tester = AT1000.open(devices[0], label="end-of-line")
tester.reset()
test = (
tester.sequence()
.hmi.screen.print("Testing…")
.hmi.screen.progress(10)
# Connect the DUT and power it
.relays.close([0, 1])
.power.dut.enable(0, 5.0)
.power.dut.wait(0, voltage=(4.8, 5.2), timeout=2.0).as_("supply")
.power.dut.read(0, repeat=5, delay=0.01, expect={"current": (None, 0.2)}).as_("current")
.hmi.screen.progress(40)
# Hold the DUT in reset, release it, wait until it is ready
.gpio.configure(0, mode="digital", direction="output", value=False, vol=0.0, voh=3.3, vil=0.8, vih=2.0)
.gpio.configure(1, mode="digital", direction="input", vil=0.8, vih=2.0)
.delay(0.01)
.gpio.write(0, True)
.gpio.wait_level(1, "high", timeout=1.0, stable_for=0.005).as_("ready")
.hmi.screen.progress(60)
# Check the DUT's clock
.gpio.pwm.input(17)
.gpio.pwm.measure(17, expect={"frequency": (9_900, 10_100)}).as_("clock")
.gpio.pwm.disable(17)
.hmi.screen.progress(80)
# Check that the DUT shows up on USB
.power.usb.enable(0)
.com.usb.wait(0, connected=True, timeout=5.0).as_("usb")
.hmi.screen.progress(100)
.hmi.screen.print("PASS")
.hmi.audio.play("success", 50)
)
try:
report = test.run(timeout=15.0)
except Exception:
tester.reset()
raise
named = report.named
print(f"Supply: {named['supply'].value.voltage:.3f} V")
for sample in named["current"].samples:
print(f"Current: {sample.current * 1000:.1f} mA")
print(f"Ready {named['ready'].elapsed * 1000:.1f} ms after the start")
print(f"Clock: {named['clock'].measurement.frequency:.0f} Hz")
print("USB:", ", ".join(str(device.product) for device in named["usb"].devices))
print(f"Whole test: {report.elapsed * 1000:.1f} ms")
use ikalogic_at1000::{
At1000, GpioConfiguration, OpenOptions, PowerBounds, PowerReadOptions, PwmExpect,
PwmMeasureOptions, ReadRepeat, SequenceOutcome, SequenceStep, Sound, WaitOptions,
};
use std::time::Duration;
let devices = At1000::find_devices(Duration::from_millis(500))?;
let tester = At1000::open(&devices[0], OpenOptions { label: Some("end-of-line".into()), ..Default::default() })?;
tester.reset()?;
let supply = PowerBounds { voltage: Some((4.8..=5.2).into()), ..Default::default() };
let current = PowerReadOptions {
repeat: Some(ReadRepeat { count: 5, delay: Some(Duration::from_millis(10)) }),
expect: Some(PowerBounds { current: Some((..=0.2).into()), ..Default::default() }),
};
let clock = PwmMeasureOptions {
expect: Some(PwmExpect { frequency: Some((9_900.0..=10_100.0).into()), ..Default::default() }),
..Default::default()
};
let test = tester.sequence()
.hmi().screen().print("Testing…")
.hmi().screen().progress(10.0)
// Connect the DUT and power it
.relays().close([0, 1])
.power().dut().enable(0, 5.0)
.power().dut().wait(0, supply, WaitOptions { timeout: Some(Duration::from_secs(2)), ..Default::default() })
.named("supply")
.power().dut().read(0, current)
.named("current")
.hmi().screen().progress(40.0)
// Hold the DUT in reset, release it, wait until it is ready
.gpio().configure(0, GpioConfiguration::DigitalOutput { value: false, vol: 0.0, voh: 3.3, vil: 0.8, vih: 2.0 })
.gpio().configure(1, GpioConfiguration::DigitalInput { vil: 0.8, vih: 2.0 })
.delay(Duration::from_millis(10))
.gpio().write(0, true)
.gpio().wait_level(1, true, WaitOptions {
timeout: Some(Duration::from_secs(1)),
stable_for: Some(Duration::from_millis(5)),
})
.named("ready")
.hmi().screen().progress(60.0)
// Check the DUT's clock
.gpio().pwm().input(17, None)
.gpio().pwm().measure(17, clock)
.named("clock")
.gpio().pwm().disable(17)
.hmi().screen().progress(80.0)
// Check that the DUT shows up on USB
.power().usb().enable(0)
.com().usb().wait(0, true, WaitOptions { timeout: Some(Duration::from_secs(5)), ..Default::default() })
.named("usb")
.hmi().screen().progress(100.0)
.hmi().screen().print("PASS")
.hmi().audio().play(Sound::Success, 50.0);
let report = match test.run(Duration::from_secs(15)) {
Ok(report) => report,
Err(error) => {
tester.reset()?;
return Err(error.into());
}
};
let step = |name: &str| report.get(name).map(|result| result.step);
if let Some(SequenceStep::PowerDutWait { value, .. }) = step("supply") {
println!("Supply: {:.3} V", value.voltage);
}
if let Some(SequenceStep::PowerDutRead { outcome: SequenceOutcome::Samples { samples }, .. }) = step("current") {
for sample in samples {
println!("Current: {:.1} mA", sample.current * 1000.0);
}
}
if let Some(ready) = step("ready") {
println!("Ready {:?} after the start", ready.elapsed());
}
if let Some(SequenceStep::GpioPwmMeasure { measurement, .. }) = step("clock") {
if let Some(stats) = &measurement.stats {
println!("Clock: {:.0} Hz", stats.frequency);
}
}
if let Some(SequenceStep::ComUsbWait { devices, .. }) = step("usb") {
println!("USB: {} device(s)", devices.len());
}
println!("Whole test: {:?}", report.elapsed());
Without a failure, the AT1000 is left as the sequence ended: the DUT stays powered and "PASS" stays on the screen until your program moves on.