[0.20.0-beta.0] — unreleased

Tracks arb 0.8.0, which replaced its three free functions with a libusb context and a board handle. This release passes that shape through to Elixir: every 0.19 entry point is gone.

Migrating from 0.19

Open a context once and hold it. Arb.open/0 costs ~6.5 ms; every other call costs ~50 µs. Before 0.20 each call paid the 6.5 ms.

# Once, when your application starts
{:ok, usb} = Arb.open()

# Free to build, resolves nothing until an operation runs
board = Arb.board(usb, port: 3)

:ok = Arb.set_relays(board, [1, 3])
{:ok, active} = Arb.relays(board)
:ok = Arb.reset_device(board)
0.190.20
Arb.activate(ids, port: p)Arb.set_relays(Arb.board(usb, port: p), ids)
Arb.activate(ids, verify: false)Arb.set_relays(board, ids, verify: false)
Arb.get_active(port: p)Arb.relays(board)no longer self-tests
Arb.reset(port: p)Arb.reset_device(board)
Arb.open/0, Arb.board/2, Arb.list_boards/1
Arb.self_test/1, the check get_active/1 used to run silently
:bad_device:self_test_failed
:verification_failed{:verification_failed, expected, actual}
{:io, msg}gone — no library path could produce it
:busy, {:unknown, msg}

Three behaviour changes the table does not show:

  • relays/1 does not self-test. get_active/1 silently wrote an inverted pattern to the shift register, read it back and undid it. If you read in order to vet the board, call self_test/1 yourself
  • A held context is not self-healing. 0.19 built a fresh one per call and so recovered from a soured libusb state by accident; 0.20 does not. Hold it somewhere swappable, per process or per board — Arb and Arb.Usb cover when to replace one and what a replacement does not fix
  • reset_device/1 returns before the board is back. :ok means the reset was issued; the board re-enumerates, so a :not_found right after is not the reset having failed

Changed (breaking)

  • Replace activate/2, get_active/1 and reset/1 with set_relays/3, relays/1 and reset_device/1, taking an Arb.Board rather than a :port option. reset/1 read like "turn all the relays off"; it is a USB reset and leaves the relay outputs untouched
  • Split the board's self-test out of the read, halving its cost — 28 USB transfers rather than 56. The check is now self_test/1
  • Give :verification_failed the relay ids it expected and read back; it previously carried nothing
  • Rename :bad_device to :self_test_failed
  • Report a board held by another application as :busy rather than {:usb, "Resource busy"}, which made it indistinguishable from a real USB fault even though it is routine and worth retrying as it stands
  • Validate :port as a byte. arb takes a u8, so a larger number previously reached the NIF and failed to decode with an opaque ArgumentError
  • Carry arb's own rendering on Arb.Error as :message instead of re-deriving it in Elixir. Errors the NIF produces read as before; a hand-built %Arb.Error{} carrying no :message now renders as its reason

Removed (breaking)

  • The {:io, message} error reason, which no library path could produce

Added

  • Arb.open/0 and Arb.Usb, the libusb context
  • Arb.board/2 and Arb.Board, a handle to one board. It holds a selector, not a device and not a USB claim, so it is free to build and never locks another application out of a shared board
  • Arb.list_boards/1, which returns every attached board in a stable order, named by where it sits on the USB tree rather than by port number. An empty list means no board is attached rather than :not_found
  • Arb.Board.port/1, and an Inspect for Arb.Board that renders #Arb.Board<port 3 (1-1.3)> — enough to tell apart two boards sharing a port number
  • Arb.self_test/1, the read-back check get_active/1 used to perform on the way past. It moves no relay, so it is safe to call on a live board
  • Arb.Error.retry_in_place?/1 and Arb.Error.relay_state_unknown?/1 — which reasons are worth another attempt as they stand, and which one leaves the relays somewhere unknown — so a caller no longer re-encodes that list and goes a release out of date. Each takes a bare reason as well as an %Arb.Error{}, for callers that cannot match the struct
  • The {:unknown, message} error reason, which is how a variant added to arb's non-exhaustive error type reaches Elixir
  • Arb.board_option/0 and Arb.set_relays_option/0, so the accepted option values are visible in the specs
  • Precompiled NIFs. Installing :arb no longer requires the Rust toolchain, and no longer requires libusb either — libusb is compiled from the copy the crate vendors and linked into the artifact, so nothing is expected of the host. Artifacts are published for Linux x86_64/aarch64 (glibc and musl) and armv7 (glibc), and for macOS aarch64/x86_64. Anywhere else it falls back to building from source, which needs Rust and a C compiler. ARB_BUILD=true forces that build on a supported target too

Fixed

Inherited from arb 0.8.0:

  • Re-attach the kernel driver when the USB interface is released. It was previously detached on open and never restored
  • Raise the USB bulk timeouts to 1000 ms, from 10 ms for reads and 100 ms for writes. Ten milliseconds for a USB round trip fails spuriously on a loaded host or through a hub, and nothing retries behind it
  • Restore the shift register when a self-test fails. The check returned on mismatch before putting the register back, so a failure made the next read disagree with the latched outputs. No relay moved either way

Earlier releases

See the git history.