A NetMD.Transport that fronts another transport with a stable pid and
transparent reconnection.
NetMD portables re-enumerate on the USB bus whenever their state changes:
inserting or ejecting a disc, powering on, or the reset that ends every
session (NetMD.Transport.Usb.close/1 mirrors netmd-js finalize, which
resets). Each re-enumeration gives the device a new USB address and, beneath
it, a new transfer engine with a new pid, so any handle held across the event
goes stale.
This transport hides that. open/1 starts a manager process and returns its
pid as the handle; the manager owns the real (base) transport handle and swaps
in a fresh one when the device returns, so the handle a caller holds never
changes. It is a drop-in NetMD.Transport, enabled by default from
NetMD.Device.open/1 (reconnect: true); pass reconnect: false for the
bare NetMD.Transport.Usb.
Reconnection is lazy: a dropped device is noticed when an operation fails, and
that operation then blocks while the manager re-opens the same device (matched
by vendor and product id) and is re-run on the new handle. So a caller's single
call rides through a re-enumeration and succeeds, waiting up to :reconnect_wait
before giving up with {:error, :disconnected}. No background polling happens
while the device is idle.
A physical re-enumeration resets the device, so protocol state that spans commands (a secure session, an in-progress TOC edit) does not survive it. Only the transport is restored, not device-side session state; re-run whole operations, not half of one.
Status events
Because the manager is a serial GenServer that already owns the device, it is
the natural place to poll status and to serialise access. When given a
:status_fun (NetMD.Device.open/1 injects NetMD.Commands.device_status/1)
it polls on a timer while at least one process is subscribed, and sends
{:netmd_status, status} to subscribers whenever the reading changes. The poll
runs inside one message handler, so it never interleaves with another manager
message.
A whole command/reply exchange spans several manager messages, though, so a
poll must not land in the middle of one. lock/1/unlock/1 (used by
NetMD.Device.with_lock/2, which wraps NetMD.Interface.send_query/3) hold the
device for an exchange; the poll defers while the lock is held. The lock is
reentrant per process and released if its holder dies.
Options (threaded through NetMD.Device.open/1):
:reconnect_wait- ms an operation waits for the device before returning{:error, :disconnected}(default10_000):reconnect_poll- ms between reopen attempts (default500):base_transport- transport to front (defaultNetMD.Transport.Usb):status_event_poll- ms between status polls, orfalseto disable (default1000):status_fun- 1-arity function read to poll status with; no polling without it
Summary
Functions
Returns a specification to start this module under a supervisor.
Hold the device for one exchange. Reentrant per process; blocks until granted.
Subscribe pid (default caller) to {:netmd_status, status} events.
Release one level of the device lock.
Stop pid (default caller) receiving status events.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec lock(pid()) :: :ok | :error
Hold the device for one exchange. Reentrant per process; blocks until granted.
Subscribe pid (default caller) to {:netmd_status, status} events.
@spec unlock(pid()) :: :ok | :error
Release one level of the device lock.
Stop pid (default caller) receiving status events.