NetMD.Transport.Managed (NetMD v0.1.0)

Copy Markdown View Source

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} (default 10_000)
  • :reconnect_poll - ms between reopen attempts (default 500)
  • :base_transport - transport to front (default NetMD.Transport.Usb)
  • :status_event_poll - ms between status polls, or false to disable (default 1000)
  • :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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

lock(manager)

@spec lock(pid()) :: :ok | :error

Hold the device for one exchange. Reentrant per process; blocks until granted.

subscribe(manager, pid \\ self())

@spec subscribe(pid(), pid()) :: :ok

Subscribe pid (default caller) to {:netmd_status, status} events.

unlock(manager)

@spec unlock(pid()) :: :ok | :error

Release one level of the device lock.

unsubscribe(manager, pid \\ self())

@spec unsubscribe(pid(), pid()) :: :ok

Stop pid (default caller) receiving status events.