[0.20.0-beta.1] — 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.19 | 0.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/1does not self-test.get_active/1silently wrote an inverted pattern to the shift register, read it back and undid it. If you read in order to vet the board, callself_test/1yourself- 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 —
ArbandArb.Usbcover when to replace one and what a replacement does not fix reset_device/1returns before the board is back.:okmeans the reset was issued; the board re-enumerates, so a:not_foundright after is not the reset having failed
Changed (breaking)
- Replace
activate/2,get_active/1andreset/1withset_relays/3,relays/1andreset_device/1, taking anArb.Boardrather than a:portoption.reset/1read 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_failedthe relay ids it expected and read back; it previously carried nothing - Rename
:bad_deviceto:self_test_failed - Report a board held by another application as
:busyrather 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
:portas a byte.arbtakes au8, so a larger number previously reached the NIF and failed to decode with an opaqueArgumentError - Carry
arb's own rendering onArb.Erroras:messageinstead of re-deriving it in Elixir. Errors the NIF produces read as before; a hand-built%Arb.Error{}carrying no:messagenow renders as its reason
Removed (breaking)
- The
{:io, message}error reason, which no library path could produce
Added
Arb.open/0andArb.Usb, the libusb contextArb.board/2andArb.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 boardArb.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_foundArb.Board.port/1, and anInspectforArb.Boardthat renders#Arb.Board<port 3 (1-1.3)>— enough to tell apart two boards sharing a port numberArb.self_test/1, the read-back checkget_active/1used to perform on the way past. It moves no relay, so it is safe to call on a live board, and it returns{:ok, ids}, the relays it found while checking. The check has to read the register before it can write its test pattern, so a caller wanting both a verdict and a state gets them from one claim instead of following it withrelays/1Arb.Error.retry_in_place?/1— which reasons are worth another attempt as they stand, so a caller no longer re-encodes that list and goes a release out of date. It 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 toarb's non-exhaustive error type reaches Elixir - The
{:register_out_of_sync, cause}error reason, for a read that could not put the shift register back (see Fixed). No relay moved, but the board's account of them is gone, so a laterrelays/1can succeed and report nothing active on a board driving eight live outputs. It is the one failure where retrying the read is the wrong answer;set_relays/3writes the register and the outputs together again Arb.board_option/0andArb.set_relays_option/0, so the accepted option values are visible in the specs- Precompiled NIFs. Installing
:arbno longer requires the Rust toolchain, and no longer requireslibusbeither — 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 Linuxx86_64/aarch64(glibc and musl) andarmv7(glibc), and for macOSaarch64/x86_64. Anywhere else it falls back to building from source, which needs Rust and a C compiler.ARB_BUILD=trueforces 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
- Put the shift register back on every path out of a read, failures included.
Reading it clocks zeros in, so every read writes back what it read; a USB error
between the two halves left the register holding zeros while the outputs held
relays. Retrying, the documented remedy for a transient USB error, is what made
it stick: the retried read succeeded and answered
{:ok, []}for an energized board. Where the contents are genuinely gone the caller now gets{:register_out_of_sync, cause}instead of the transport error that invited the retry - 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
- Put the latched value back into the shift register after a failed verification, rather than the value that was read back. A mismatch implicates the read path, so leaving its answer in the register made the next read agree with the fault instead of with the relays
Earlier releases
See the git history.