microscrap/usb-drivers

The Official ScrapyardIO USB Drivers package.

Maintainers

Package info

github.com/microscrap/scrapyard-usb-drivers

Homepage

Issues

pkg:composer/microscrap/usb-drivers

Transparency log

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

dev-main 2026-07-06 17:32 UTC

This package is auto-updated.

Last update: 2026-07-06 19:35:21 UTC


README

PHP driver package that provides the usb carrier for the ScrapyardIO framework GPIO stack. It drives FTDI USB adapters (FT232H, FT2232HL, FT232RL) through ext-ftdi, tunnelling SPI, I²C, and digital GPIO over MPSSE and running UART as plain async serial.

This package includes:

  • The ScrapyardIOUSBManager carrier manager, auto-discovered by the framework via the #[CarrierDriver('usb')] attribute
  • MPSSE-backed protocol drivers for SPI, I²C, and digital input/output
  • A libftdi-backed UART driver (no MPSSE) for serial-only workflows
  • Digital pin ride-along on SPI/I²C buses — control DC/RESET/BUSY pins on the same FTDI adapter that carries the bus

Requirements

  • PHP 8.3+
  • ext-ftdi ^0.4.2 — the PHP FTDI extension (requires libftdi1 at runtime)
    • Debian/Ubuntu/Raspberry Pi OS: libftdi1-2 (dev package for builds: libftdi1-dev)
    • macOS: brew install libftdi
  • microscrap/ftdi ^0.5.0 — global ftdi_* helpers (installed automatically)
  • microscrap/mpsse ^0.5.0 — required for SPI, I²C, and digital GPIO (suggested; UART works without it)

Currently Supported devices

Device MPSSE device() value (SPI/I²C/GPIO) UART device() value Product ID
FT232H ft232h FtdiProductId::FT232H->value 0x6014
FT2232HL ft2232hl-a (channel A) / ft2232hl-b (channel B) FtdiProductId::FT2232HL->value 0x6010
FT232RL — (no MPSSE engine) FtdiProductId::RS232L->value 0x6001

Note, these are the only devices so far I've tested. There shouldn't be any reason why any other MPSSE-enabled devices wouldn't work if there is a valid product id hex for the device. You could make a pull request with the missing device, or use custom logic that doesn't use the MpsseSupportedDevices enum.

The two protocol families address devices differently:

  • SPI / I²C / GPIO go through Microscrap\Bindings\MPSSE\Enums\MpsseSupportedDevice, which picks the exact MPSSE engine (channel A or B) and its product ID together. The FT2232HL has two independent MPSSE engines, so ft2232hl-a and ft2232hl-b are addressed separately.
  • UART goes through Microscrap\Bindings\FTDI\Enums\FtdiProductId, which is only the raw product ID — there is one UART per chip regardless of how many MPSSE engines it has, so the FT2232HL is addressed as a single FtdiProductId::FT2232HL, not per channel.

Installation

Install the FTDI extension first (see php-io-extensions/ftdi):

pie install php-io-extensions/ftdi

Confirm it is loaded:

php -m | grep ftdi

Install the driver package:

composer require microscrap/usb-drivers

If your workflow includes SPI, I²C, or digital GPIO (anything other than UART), install the MPSSE package as well:

composer require microscrap/mpsse

The manager enforces these prerequisites at runtime and throws a GPIOException with install instructions when one is missing.

How it works

The framework resolves the usb carrier through attribute-based discovery — no manual registration:

GPIO::spi('usb')->…->create()
  └─ SPIConnectionFactory            (scrapyard-io/framework)
       └─ GPIOCarriers::usb('spi')
            └─ ScrapyardIOUSBManager  (#[CarrierDriver('usb')], this package)
                 └─ MPSSESPIDriver
                      └─ microscrap/mpsse → microscrap/ftdi → ext-ftdi → libftdi1

Each GPIO protocol maps to a driver in this package:

Protocol Driver Backend
spi MPSSESPIDriver MPSSE, modes SPI0–SPI3
i2c MPSSEI2CDriver MPSSE with full ACK/NACK sequencing
digital-in MPSSEDigitalInputDriver MPSSE GPIO mode, polled edge events
digital-out MPSSEDigitalOutputDriver MPSSE GPIO mode
uart FTDIUartDriver Plain async serial via ftdi_* helpers — no MPSSE

UART is intentionally kept off MPSSE: FTDIUartDriver resets the chip to async-serial bitmode on open (ftdi_set_bitmode($context, 0x00, 0x00)) so that multi-protocol parts like the FT232H release D0/D1 back to TX/RX even if a previous session left MPSSE enabled.

Usage

All examples go through the framework's GPIO facade with the usb driver.

SPI

<?php

use GPIO\Common\GPIO;
use GPIO\Contracts\SPI\SPIMode;

$spi = GPIO::spi('usb')
    ->device('ft232h')
    ->mode(SPIMode::MODE_0)
    ->speed(1_000_000)
    ->create();

$spi->write("\x9F");                          // clock bytes out
$jedec = $spi->transfer("\x9F\x00\x00\x00");  // full-duplex transfer
$bytes = $spi->read(3);                       // clock bytes in

$spi->close();

I²C

<?php

use GPIO\Common\GPIO;

$i2c = GPIO::i2c('usb')
    ->device('ft232h')
    ->slave(0x26)
    ->create();

$who_am_i = $i2c->writeRead("\x01", 1);  // write register address, repeated START, read 1 byte
$i2c->write([0x11, 0x40]);               // write register + value
$data = $i2c->read(2);                   // plain read

$i2c->close();

UART

UART devices are addressed by FTDI product ID, not by MPSSE device name:

<?php

use GPIO\Common\GPIO;
use Microscrap\Bindings\FTDI\Enums\FtdiProductId;

$uart = GPIO::uart('usb')
    ->device(FtdiProductId::RS232L->value)  // 0x6001 — FT232RL
    ->baud(115200)
    ->create();

$uart->write("AT\r\n");
$response = $uart->read(64);  // byte array, or false when nothing arrived

$uart->flush();
$uart->close();

The same call works on a FT2232HL — pass FtdiProductId::FT2232HL->value instead of an ft2232hl-a / ft2232hl-b MPSSE string, since the chip exposes only one UART regardless of channel:

$uart = GPIO::uart('usb')
    ->device(FtdiProductId::FT2232HL->value)  // 0x6010
    ->baud(115200)
    ->create();

baud(), parity(), stopBits(), dataBits(), and flowControl() are all available on the factory; defaults are 9600 8N1 with no flow control.

Digital output

GPIO mode exposes 12 lines per MPSSE device — MPSSEGpioPin::GPIOL0GPIOL3 (pins 0–3) and GPIOH0GPIOH7 (pins 4–11):

<?php

use GPIO\Common\GPIO;
use Microscrap\Bindings\MPSSE\Enums\MPSSEGpioPin;

$led = GPIO::digitalOut('usb')
    ->device('ft232h')
    ->pin(MPSSEGpioPin::GPIOL1->value)
    ->name('led')
    ->defaultState(false)
    ->create();

$led->high();
$led->low();

$led->close();

Digital input

<?php

use GPIO\Common\GPIO;

$button = GPIO::digitalIn('usb')
    ->device('ft232h')
    ->pin(0)
    ->name('button')
    ->withEvents(true, false)  // rising, falling
    ->timeout(5000)            // ms
    ->create();

$event = $button->listen();     // ?DigitalInputEvent — blocks up to the timeout
$pressed = $button->isHigh();   // immediate level read

$button->close();

Edge detection is polling-based (the FTDI adapter has no interrupt lines), sampling roughly every millisecond until the timeout elapses.

Digital pins on a SPI/I²C bus

Displays and similar chips need DC/RESET/BUSY pins alongside the bus. Attach them to the same MPSSE context and get a connection bus back:

<?php

use GPIO\Common\GPIO;
use GPIO\Contracts\SPI\SPIMode;
use GPIO\Contracts\Digital\LineBias;

$bus = GPIO::spi('usb')
    ->device('ft232h')
    ->mode(SPIMode::MODE_0)
    ->speed(1_000_000)
    ->digitalPins()
    ->digitalOut(pin: 4, name: 'dc', default_state: false)
    ->digitalIn(pin: 5, name: 'busy', events: [false, true], timeout: 1000, bias: LineBias::AS_IS)
    ->create();  // SPIConnectionBus

$bus->getPin('dc')->high();
$bus->spi->write("\x2A");

while ($bus->getPin('busy')->isHigh()) {
    usleep(1_000);
}

The same pattern works for I²C (I2CConnectionBus, with the transport on $bus->i2c).

Exceptions

Exception Extends Thrown when
MpsseUSBException GPIOException An MPSSE device context cannot be opened (includes the FTDI error string)
FTDIUARTException UARTException A UART device fails to open (includes the FTDI error string)
UARTException The factory's device() was never set (missingMasterDevice)
GPIOException ext-ftdi, microscrap/ftdi, or microscrap/mpsse is missing, or a driver does not extend ScrapyardIOUSBDriver

Note that passing an unknown product ID to GPIO::uart('usb')->device(...) fails inside FtdiProductId::from() with a PHP ValueError before any exception above is reached.

Testing (Pest v4)

Run the suite:

./vendor/bin/pest

The unit tests run without hardware — FTDI and MPSSE globals are replaced with in-memory fakes and spies (tests/Support/), so driver dispatch, MPSSE call ordering, and error paths are all exercised on any machine.

License

MIT. See LICENSE.