alicatlib.transport¶
The Transport Protocol, SerialTransport for hardware, and
FakeTransport for tests. See Design §5.1.
Public surface¶
alicatlib.transport ¶
Transport layer — moves bytes, knows nothing about Alicat.
See docs/design.md §5.1.
FakeTransport ¶
Scripted :class:Transport for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[bytes, ScriptedReply] | None
|
Mapping of |
None
|
label
|
str
|
Identifier used in errors. |
'fake://test'
|
latency_s
|
float
|
Per-operation artificial delay, useful for simulating a slow device. |
0.0
|
Source code in src/alicatlib/transport/fake.py
last_reopen_baud
property
¶
Baud rate requested by the most recent :meth:reopen, or None.
add_script ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that was left streaming, or garbage on the line that the session must drain on recovery.
Source code in src/alicatlib/transport/fake.py
force_read_timeout ¶
force_reopen_error ¶
Force the next :meth:reopen to raise :class:AlicatConnectionError.
Used by :class:Session.change_baud_rate tests to exercise
the BROKEN-state transition without a real serial adapter.
Source code in src/alicatlib/transport/fake.py
force_write_timeout ¶
reopen
async
¶
Simulate a baud-rate change — close, record, reopen.
If force_reopen_error() has been called the reopen raises
:class:AlicatConnectionError after the close, leaving the
transport closed. That's the "reopen wedged" path tested at
the session layer for :attr:SessionState.BROKEN transitions.
Source code in src/alicatlib/transport/fake.py
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=19200,
bytesize=ByteSize.EIGHT,
parity=Parity.NONE,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration.
Mirrors :class:anyserial.SerialConfig plus a port path. exclusive
defaults True so that two processes can't scribble over the same
device — the Alicat wire protocol isn't multi-master tolerant.
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware can use
:class:alicatlib.transport.fake.FakeTransport instead; the two conform
to the same structural :class:Transport protocol.
Source code in src/alicatlib/transport/serial.py
reopen
async
¶
Close and reopen the port at baudrate.
Called by :meth:Session.change_baud_rate after the device
has already switched — the transport has to retune to stay in
sync. The cached :class:SerialSettings is updated so the
new baud survives subsequent lifecycle calls (close +
future open round-trip at the same rate).
If :meth:open fails on the new baud the transport is left
closed; the caller (the session's baud-change shield) is
responsible for surfacing that as a BROKEN session state
with recovery guidance.
Source code in src/alicatlib/transport/serial.py
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry, implementations
raise :class:alicatlib.errors.AlicatTimeoutError — never return an empty
or partial bytes silently. Backend exceptions normalize to
:class:alicatlib.errors.AlicatTransportError (or a subclass) with
__cause__ preserving the original exception.
Lifecycle is single-shot: :meth:open once, :meth:close once.
close
async
¶
drain_input
async
¶
open
async
¶
read_available
async
¶
Read until the line goes idle for idle_timeout seconds.
Never raises on idle expiry — an idle timeout is the expected exit. Returns whatever was accumulated (possibly empty). Used for best-effort drain / stream-stop recovery, not for request/response.
Source code in src/alicatlib/transport/base.py
read_until
async
¶
Read bytes up to and including the next occurrence of separator.
Raises :class:alicatlib.errors.AlicatTimeoutError if the separator
does not arrive before timeout. Bytes received after the separator
remain buffered for the next call — implementations must not discard
them.
Source code in src/alicatlib/transport/base.py
reopen
async
¶
Close and re-open the underlying port at a new baud rate.
Used by :meth:Session.change_baud_rate to retune the port
after the device has already switched baud rates mid-sequence
(primer NCB command — see design §5.7). Serial transports
close the port, update the cached settings, and open at the
new baud; non-serial transports (TCP, future) may raise
:class:NotImplementedError — baud rates don't apply there.
Implementations must leave the transport in a consistent state: either fully reopened at the new baud, or clearly closed so callers can recognise a failure. Silent partial states are the worst failure mode for this method.
Source code in src/alicatlib/transport/base.py
write
async
¶
Write every byte of data. Raise AlicatTimeoutError on expiry.
A bounded write timeout is mandatory because sends can block on RS-485 hardware flow control, a stuck device, or (on TCP) a full send buffer. Callers that block indefinitely hide real hangs.
Source code in src/alicatlib/transport/base.py
Base Protocol + serial settings¶
alicatlib.transport.base ¶
Transport layer abstraction — moves bytes, knows nothing about Alicat.
The :class:Transport :pep:544 Protocol is the interface every backend
implements. :class:SerialSettings is the port-configuration dataclass
consumed by :class:alicatlib.transport.serial.SerialTransport.
Design reference: docs/design.md §5.1.
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=19200,
bytesize=ByteSize.EIGHT,
parity=Parity.NONE,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration.
Mirrors :class:anyserial.SerialConfig plus a port path. exclusive
defaults True so that two processes can't scribble over the same
device — the Alicat wire protocol isn't multi-master tolerant.
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry, implementations
raise :class:alicatlib.errors.AlicatTimeoutError — never return an empty
or partial bytes silently. Backend exceptions normalize to
:class:alicatlib.errors.AlicatTransportError (or a subclass) with
__cause__ preserving the original exception.
Lifecycle is single-shot: :meth:open once, :meth:close once.
close
async
¶
drain_input
async
¶
open
async
¶
read_available
async
¶
Read until the line goes idle for idle_timeout seconds.
Never raises on idle expiry — an idle timeout is the expected exit. Returns whatever was accumulated (possibly empty). Used for best-effort drain / stream-stop recovery, not for request/response.
Source code in src/alicatlib/transport/base.py
read_until
async
¶
Read bytes up to and including the next occurrence of separator.
Raises :class:alicatlib.errors.AlicatTimeoutError if the separator
does not arrive before timeout. Bytes received after the separator
remain buffered for the next call — implementations must not discard
them.
Source code in src/alicatlib/transport/base.py
reopen
async
¶
Close and re-open the underlying port at a new baud rate.
Used by :meth:Session.change_baud_rate to retune the port
after the device has already switched baud rates mid-sequence
(primer NCB command — see design §5.7). Serial transports
close the port, update the cached settings, and open at the
new baud; non-serial transports (TCP, future) may raise
:class:NotImplementedError — baud rates don't apply there.
Implementations must leave the transport in a consistent state: either fully reopened at the new baud, or clearly closed so callers can recognise a failure. Silent partial states are the worst failure mode for this method.
Source code in src/alicatlib/transport/base.py
write
async
¶
Write every byte of data. Raise AlicatTimeoutError on expiry.
A bounded write timeout is mandatory because sends can block on RS-485 hardware flow control, a stuck device, or (on TCP) a full send buffer. Callers that block indefinitely hide real hangs.
Source code in src/alicatlib/transport/base.py
Serial transport¶
alicatlib.transport.serial ¶
Serial-port transport backed by :mod:anyserial.
:class:SerialTransport wraps :class:anyserial.SerialPort. Every I/O call
is bounded by :func:anyio.fail_after (reads, writes) or
:func:anyio.move_on_after (idle-timeout reads). Backend exceptions
normalize to :mod:alicatlib.errors types with __cause__ preserved.
Design reference: docs/design.md §5.1.
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware can use
:class:alicatlib.transport.fake.FakeTransport instead; the two conform
to the same structural :class:Transport protocol.
Source code in src/alicatlib/transport/serial.py
reopen
async
¶
Close and reopen the port at baudrate.
Called by :meth:Session.change_baud_rate after the device
has already switched — the transport has to retune to stay in
sync. The cached :class:SerialSettings is updated so the
new baud survives subsequent lifecycle calls (close +
future open round-trip at the same rate).
If :meth:open fails on the new baud the transport is left
closed; the caller (the session's baud-change shield) is
responsible for surfacing that as a BROKEN session state
with recovery guidance.
Source code in src/alicatlib/transport/serial.py
Fake transport¶
alicatlib.transport.fake ¶
In-process fake transport for tests.
:class:FakeTransport implements the :class:Transport Protocol without
touching a serial port. Tests script the expected write→response mapping and
assert the recorded command bytes.
Design reference: docs/design.md §5.1.
FakeTransport ¶
Scripted :class:Transport for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[bytes, ScriptedReply] | None
|
Mapping of |
None
|
label
|
str
|
Identifier used in errors. |
'fake://test'
|
latency_s
|
float
|
Per-operation artificial delay, useful for simulating a slow device. |
0.0
|
Source code in src/alicatlib/transport/fake.py
last_reopen_baud
property
¶
Baud rate requested by the most recent :meth:reopen, or None.
add_script ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that was left streaming, or garbage on the line that the session must drain on recovery.
Source code in src/alicatlib/transport/fake.py
force_read_timeout ¶
force_reopen_error ¶
Force the next :meth:reopen to raise :class:AlicatConnectionError.
Used by :class:Session.change_baud_rate tests to exercise
the BROKEN-state transition without a real serial adapter.
Source code in src/alicatlib/transport/fake.py
force_write_timeout ¶
reopen
async
¶
Simulate a baud-rate change — close, record, reopen.
If force_reopen_error() has been called the reopen raises
:class:AlicatConnectionError after the close, leaving the
transport closed. That's the "reopen wedged" path tested at
the session layer for :attr:SessionState.BROKEN transitions.