Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft 1.1.3 public beta released 08-Nov-2026. The Free Plan and the Team trial are open; card payments and invoices open shortly.

Orcraft

The platform for test development and orchestration
that lets you and your AI Agents run the entire testing operation

Watch the demo

FTDI Suite

The tutorial suite ran against the Maze application, a program, and the previous page moved to the network between machines. A test lab usually has hardware on the bench too: a board wired to the machine that talks to it, sometimes to two machines that have to act together. The FTDI GPIO example suite is a small suite of that kind. It drives the FT2232H Mini Module, an inexpensive USB board from FTDI, as a set of GPIO pins from the endpoints: one module on one machine blinks LEDs and reads its own pins back, and two modules on two machines, wired to each other, carry text messages across a handful of wires with a handshake.

The suite is a separate repository laid out exactly like the tutorial suite. Clone it next to the tutorial suite and add its entry point, test_suite_ftdi.py, to the controller the same way the Getting started guide adds the tutorial suite:

git clone https://github.com/GMCoreAI/orcraft-test-suite-ftdi.git

Beyond what the tutorial suite shows, this one demonstrates:

  • Suite requirements: the Python packages the suite needs are installed on the endpoints by the controller, from a requirements.txt at the root of the repository.
  • One hardware handle for a whole test: the module is opened once in _setup(), kept open on the endpoint, handed to every step of every case, and closed in _teardown().
  • Two endpoints coordinated at wire level: a receiver waits in the background on one machine while a sender drives the wires from the other, and the flow collects what arrived.

Hardware

The LEDs group needs one module on one endpoint; the GPIO Link group needs a second module on a second endpoint. The same wiring serves every test, so build it once:

PartQuantityNotes
FT2232H Mini Module1 or 2One for the LEDs group, two for the GPIO Link group.
USB mini-B cable1 per moduleThe module is powered from USB.
Breadboard1 per moduleHolds the LEDs and resistors.
LED6 per moduleOne on each of AD0 to AD5.
Resistor, 330 to 470 ohm6 per module, plus 6 for the linkOne in series with each LED, and one in series with each wire between the modules.
Jumper wiresa handfulFive short ones per module for its power jumpers, seven between the modules.

Module setup

The Mini Module has two 26-pin headers, CN2 and CN3, and ships with nothing connected between its power rails. The chip enumerates on USB only once the USB 5 V (VBUS) is jumpered to the regulator input (VCC), and its pins only go high once the regulator's 3.3 V output (V3V3) is jumpered to every I/O bank supply input (VIO). Five short jumper wires do that:

JumperFromToPurpose
J1CN3-1 (VBUS)CN3-3 (VCC)Powers the module from the USB port.
J2CN2-1 (V3V3)CN2-11 (VIO)Feeds an I/O bank from the 3.3 V regulator.
J3CN2-3 (V3V3)CN2-21 (VIO)Feeds an I/O bank from the 3.3 V regulator.
J4CN2-5 (V3V3)CN3-12 (VIO)Feeds an I/O bank from the 3.3 V regulator.
J5CN3-12 (VIO)CN3-22 (VIO)Chains the last I/O bank to the same supply.

The suite tells the two mistakes apart. Without J1 the module never appears on the USB bus and the Verify Module step fails with Module auto was not found on this endpoint. Without J2 to J5 the module is found and opens, but every pin stays low whatever is written to it, so LED Blink fails with the pin not following the written level:

Channel A pins

The suite uses channel A of the chip only, the pins printed AD0 to AD7 on CN2; channel B stays free. The pin numbers zigzag because the odd pins sit in one row of the header and the even pins in the other:

SignalCN2 pinLEDs groupGPIO Link group
AD07LED 1DATA0
AD110LED 2DATA1
AD29LED 3DATA2
AD312LED 4DATA3
AD414LED 5REQ
AD513LED 6ACK
AD6, AD716, 15unusedunused
GND2, 4 or 6LED returncommon ground

Wiring

Every test runs on one wiring. The LEDs group uses a module on its own; the GPIO Link group joins two such modules with straight wires, and the same LEDs then show the traffic. Build the LED side on each module first, then join the modules.

LEDs on one module

From each of AD0 to AD5, an LED with its own series resistor to GND: the LED's anode to the pin, its cathode through the resistor to ground. A freshly plugged module sits in its UART mode, in which AD0, AD2 and AD4 idle high, so LEDs 1, 3 and 5 are lit until the suite opens the module; that is expected. The suite closes the module with every pin as an input, so after a run all LEDs are dark.

The two modules are wired pin to pin, AD0 to AD0 and so on up to AD5, plus the two grounds joined. Which side drives which wire follows who is sending, so the same wires carry messages both ways:

Test moduleAux moduleSignalDriven by
AD0 to AD3AD0 to AD3DATA, one nibblethe sending side
AD4AD4REQ, a nibble is readythe sending side
AD5AD5ACK, nibble takenthe receiving side
GNDGNDcommon groundalways

Put a 330 to 470 ohm resistor in series with each of the six signal wires. A freshly plugged module drives AD0, AD2 and AD4 high in its UART mode, and until the suite opens both modules two outputs can face each other on one wire; the resistor keeps that clash under 10 mA, which the pins tolerate, while an input still reads the level cleanly. The LEDs on the wires make a transfer visible: the four data LEDs show each nibble as it lands and the REQ and ACK LEDs show the handshake. At a slow pace the message can be read off the board.

Endpoint preparation

An endpoint that runs this suite needs two things the tutorial suite never asked for: Python packages that talk to the module, and an operating system that lets those packages reach the USB device.

Suite requirements

The suite drives the module through pyftdi, which speaks to the chip over libusb. Windows ships no libusb, so the suite also lists libusb-package, a wheel that carries the library. Both are named in the requirements.txt at the root of the repository:

pyftdi
libusb-package

Nothing is installed by hand. After the suite package is uploaded to an endpoint, the controller has the endpoint application's Python install the file with pip, once per uploaded package, and reports the outcome in the log next to the upload lines. The mechanism is described in the framework's Suite requirements section. A test that runs on localhost uses the controller's own Python, where the packages must be installed by hand.

WinUSB driver on Windows

When a Mini Module is plugged into a Windows machine, Windows binds FTDI's own serial driver to it and offers two COM ports. That driver does not let libusb open the device, so the first run on such an endpoint stops at the Verify Module step:

The fix is a one-time driver switch with Zadig, a small tool that replaces the driver of one USB interface with WinUSB, the generic driver libusb works with. On the endpoint machine, with the module plugged in:

  1. Download and start Zadig. It needs no installation.
  2. Open Options and tick List All Devices, so devices that already have a driver are shown.
  3. Pick Dual RS232-HS (Interface 0) in the device list; the USB ID reads 0403 6010.
  4. Leave WinUSB as the target driver and click Replace Driver.
  5. Pick Dual RS232-HS (Interface 1) and click Replace Driver again.

Switch both interfaces, not only Interface 0. The suite uses channel A, but the two channels of one module are two interfaces of one USB device, and libusb opens the device as a whole. A module with one interface on WinUSB and the other still on the FTDI driver does not always enumerate the same way after a replug or a reboot, and then the open fails again with the same message. With both on WinUSB the module is stable.

Zadig takes a few seconds per interface. When an interface is selected again afterwards, both sides show WinUSB and the button reads Reinstall Driver, which is how a prepared machine looks for Interface 0 and for Interface 1 alike:

Run the test again; it now opens the module. The switch holds across reboots and applies to that module on that machine, so a second module or a second machine needs its own pass. The module no longer appears as a COM port while it has the WinUSB driver; to give it back to FTDI's driver, uninstall the device in the Device Manager with its driver and plug it in again.

USB access on Linux

Linux needs no driver change, because pyftdi detaches the kernel's serial driver from the interface by itself. What it needs is permission: by default only root may open a USB device, and the endpoint application runs as an ordinary user. A udev rule opens the FTDI devices to every user:

SUBSYSTEM=="usb", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6010", MODE="0666"

Save it as /etc/udev/rules.d/99-ftdi.rules, reload the rules and plug the module in again:

sudo udevadm control --reload-rules && sudo udevadm trigger

Suite structure

The repository follows the tutorial suite's layout: one file per suite, group and test, the flows of a test under its folder, and everything several tests share under common:

orcraft-test-suite-ftdi/
	requirements.txt
	test_suite_ftdi.py
	common/
		ftdi_gpio.py          the module as eight GPIO pins, through pyftdi
		gpio_link.py          the nibble protocol: frame, REQ/ACK handshake, sender and receiver
		module_test.py        base test for one module on one endpoint
		link_test.py          base test for two modules on two endpoints
		flows/                open module, close module, transfer message
		steps/                verify, open, close, blink, drive pattern, set and read pins,
		                      start receiver, send message, wait receiver
	test_group_leds/
		test_led_blink/
		test_led_pattern/
	test_group_link/
		test_wire_map/
		test_send_message/
		test_receive_message/
		test_exchange_messages/

One handle for the whole test

Source file:

orcraft-test-suite-ftdi/common/module_test.py

Opening a USB device is slow and disturbs the pins, so a test does it once. The base class of the LEDs tests opens the module in _setup(), keeps the handle for every case, and closes it in _teardown(). No step opens or closes the module itself:

class ModuleTest(OrcraftTest):
	direction = 0x00

	def __init__(self, config: ModuleTestConfig) -> None:
		super().__init__(config)
		self.gpio = None

	def _setup(self) -> None:
		flow = FlowOpenModule(
			FlowOpenModuleConfig(
				endpoint=self.config.test_endpoint,
				module_serial=self.config.module_serial,
				direction=self.direction,
			)
		)
		flow_result = flow.run()

		# Raising from _setup aborts the test: no cases run and _teardown is skipped.
		if self.handle_errors(flow_result, self.result):
			raise RuntimeError(self.result.error_description or "Setup could not open the module.")

		self.gpio = flow_result.gpio

	def _teardown(self) -> None:
		flow = FlowCloseModule(
			FlowCloseModuleConfig(
				endpoint=self.config.test_endpoint,
				gpio=self.gpio,
			)
		)
		flow.run()

The handle never leaves the endpoint. The Open Module step creates it there and puts it on its result, and from then on the test passes that object to every step that targets the same endpoint, the way it passes any other value. What travels to the controller is a reference to the object, valid for as long as the test holds the endpoint, so a later step receives the very same open handle and works with it directly:

def _run_flow(self) -> None:
	step_1_result = self.run_step(
		StepBlinkLed,
		endpoint=self.config.endpoint,
		gpio=self.config.gpio,
		pin=self.config.pin,
		period_ms=self.config.period_ms,
		blinks=self.config.blinks,
	)

	self.result.blinks_done = step_1_result.blinks_done

	self.handle_errors(step_1_result, self.result)

A step that needs the pins the other way round, an input where the previous step drove an output, changes the direction on the open handle instead of reopening it. That is what lets the link tests turn a module from the receiving side into the sending side between two flows without touching the wiring.

LEDs group

Both tests of the group take one endpoint and one module. Select the test, set Test Endpoint to the machine the module is plugged into and leave Module Serial at auto, which takes the first FT2232H found on that machine. With two modules on one machine, the log of the Verify Module step lists every module it found, by serial number or, for a module with a blank EEPROM, by its USB position such as bus 1 address 2; either name can be entered as the serial.

Source directory:

orcraft-test-suite-ftdi/test_group_leds/test_led_blink/

The test opens the module with every pin as an output and blinks one LED per case, with a matrix of the pin and the period:

blinks = OrcraftConfigField.Numeric(default=5, label="Blinks")

matrix = OrcraftConfigField.Matrix(
	OrcraftConfigField.Dimension("pin", (0, 1, 2, 3, 4, 5)),
	OrcraftConfigField.Dimension("period_ms", (200, 500)),
)

Each case drives its pin high and low for the number of blinks and reads the pin back after every write. The LED on the bench blinks, and the case passes when the module reported the level it was told to set every time. That read-back is what turns a demonstration into a test: the failure at the top of this page, a module whose I/O banks had no power, was found by it.

LED Pattern

Source directory:

orcraft-test-suite-ftdi/test_group_leds/test_led_pattern/

The second test drives all six LEDs at once and walks patterns over them, one sequence per case at one of two periods: a walking one lights each LED in turn, a walking zero darkens each in turn, and a count shows the numbers 0 to 63 in binary. Every pattern is read back after it is written, exactly as in LED Blink.

The link group needs both modules. Every test takes the Test Endpoint and the Aux Endpoint, the two machines, a Module Serial for each, the Message to send and a Pace ms that holds each nibble on the wires a little longer so the LEDs stay readable; set it to 0 for the fastest transfer. Both modules are opened in _setup() with every pin as an input, so nothing drives the shared wires until a flow assigns the roles.

A message crosses the wires one nibble at a time, with a four-phase handshake:

  1. The sender puts the nibble on DATA0 to DATA3 and raises REQ.
  2. The receiver reads the nibble and raises ACK.
  3. The sender lowers REQ.
  4. The receiver lowers ACK, and the sender may put the next nibble on the wires.

Each machine polls its USB port at its own speed, and the handshake means neither side can outrun the other. A frame is one length byte followed by the UTF-8 bytes of the message, so the receiver knows when the message is complete.

Wire Map

Source directory:

orcraft-test-suite-ftdi/test_group_link/test_wire_map/

Run this test first, before any message. One module raises each of AD0 to AD5 in turn while the other reads all its pins, then the roles swap. A correct link maps every pin to the same pin on the other side and nothing else, and the test's Wire Map result spells the mapping out, pin by pin, in both directions:

Test to aux: AD0 -> AD0; AD1 -> AD1; AD2 -> AD2; AD3 -> AD3; AD4 -> AD4; AD5 -> AD5
Aux to test: AD0 -> AD0; AD1 -> AD1; AD2 -> AD2; AD3 -> AD3; AD4 -> AD4; AD5 -> AD5

A swapped, missing or shorted wire shows up by name: a missing wire maps to nothing, a swapped pair maps each pin to the other, and a short between two pins of the driving module is reported as that module raising its own second pin.

Send, Receive and Exchange

Source directory:

orcraft-test-suite-ftdi/test_group_link/test_send_message/

The three message tests share one flow, Transfer Message, and differ only in who sends. Send Message sends from the test endpoint to the aux endpoint, Receive Message the other way round, and Exchange Messages runs the flow twice on the same two handles: the message one way, then a Reply back. The flow has three steps:

def _run_flow(self) -> None:
	# Step 1: start listening on the receiver; the step returns at once
	step_1_result = self.run_step(
		StepStartReceiver,
		endpoint=self.config.receiver_endpoint,
		gpio=self.config.receiver_gpio,
		timeout_sec=RECEIVE_TIMEOUT_SEC,
	)

	if self.handle_errors(step_1_result, self.result):
		return

	# Step 2: send the message from the sender
	step_2_result = self.run_step(
		StepSendMessage,
		endpoint=self.config.sender_endpoint,
		gpio=self.config.sender_gpio,
		message=self.config.message,
		pace_ms=self.config.pace_ms,
	)

	# Step 3: collect what the receiver got, even when the sender reported trouble
	step_3_result = self.run_step(
		StepWaitReceiver,
		endpoint=self.config.receiver_endpoint,
		task=step_1_result.task,
		duration_sec=WAIT_AFTER_SEND_SEC,
	)

The receiver has to be listening before the first nibble arrives, so the flow starts it first. Start Receiver makes its handle the receiving side, ACK as the only output, starts a thread on the aux endpoint that waits for REQ, and returns at once with the running task on its result, the pattern from Handling Asynchronous Steps. Send Message then makes the other handle the sending side, DATA and REQ as outputs, and drives the message. Wait Receiver goes back to the aux endpoint with the task, waits for the thread to finish and reads the message it collected. Select Send Message, set the two endpoints, click Update Plan, Run, and follow the flow in the log:

The sender reports 19 bytes for an 18-character message, the length byte plus the text, at a pace of 50 ms per nibble. The flow ends with a checkpoint that compares the received text with the sent one, and the test's result carries the Received Message and the Bytes Per Sec. A transfer that stalls fails with what the receiver had collected by then, nibble by nibble, so a wiring fault points at the wire.

With both machines prepared and the wiring in place, the whole suite runs green. Each LEDs test names the one machine its module is on, each link test names both, and the run panel on the right shows the same endpoint and module fields the tests were configured with:

This suite is the shape of a lab test: hardware on the bench, driven from the machines it is plugged into, with the framework moving the code, the requirements and the results. An AI agent can extend it the same way it extends any suite, from the files the controller exports.

Type to search the docs.

    ↑↓ navigate ↵ open Esc close