watlowlib.transport¶
The Transport Protocol, SerialTransport for hardware, and
FakeTransport for tests. See Design §4.
Public surface¶
watlowlib.transport ¶
Transport layer — moves bytes; knows nothing about Watlow.
The :class:Transport Protocol is the structural interface every
backend implements. :class:SerialSettings is the port-configuration
dataclass consumed by :class:SerialTransport. Tests use
:class:FakeTransport instead.
See docs/design.md §3 / §4.
FakeSlave ¶
Scripted :class:anymodbus.Slave stand-in for tests.
Mirrors the surface :class:watlowlib.protocol.modbus.client.ModbusProtocolClient
actually calls (read_holding_registers,
read_input_registers, write_register, write_registers)
and records every call. Tests assert on :attr:reads and
:attr:writes to verify the :class:ModbusOp lowered correctly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[tuple[str, int], ScriptedSlaveEntry] | None
|
|
None
|
Source code in src/watlowlib/transport/fake.py
add_script ¶
Register or overwrite a scripted reply for (method, address).
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/watlowlib/transport/fake.py
unmatched_writes
property
¶
Writes that didn't match any scripted reply, in order.
A test can assert transport.unmatched_writes == () to catch
accidentally-unscripted traffic — the corresponding read would
have timed out, but a precise assertion fails faster and points
at the right call.
add_script ¶
extend_queue ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that left chatter on the line which the protocol client has to drain on recovery.
Source code in src/watlowlib/transport/fake.py
force_read_timeout ¶
force_write_timeout ¶
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=38400,
bytesize=ByteSize.EIGHT,
parity=Parity.NONE,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration for :class:SerialTransport.
Mirrors :class:anyserial.SerialConfig plus a port path. Default
framing is 38400 8-N-1, the EZ-ZONE PM Standard Bus factory
setting. exclusive defaults True because Standard Bus is
poll/response and won't tolerate a second writer.
The __post_init__ accepts int / float / str shorthand
at runtime for the framing fields (bytesize=8, parity="none",
stopbits=1) and normalises to the enum. The static field types
are the enums themselves so mypy --strict users must pass
:class:anyserial.ByteSize / :class:anyserial.Parity /
:class:anyserial.StopBits directly; the runtime shorthand is
primarily for CLI argument parsing and interactive scripts.
factory_for
classmethod
¶
Return the EZ-ZONE PM factory framing for protocol.
STDBUS→ 38400 8-N-1 (the Standard Bus factory default).MODBUS_RTU→ 9600 8-E-1 (the Modbus RTU factory default per the EZ-ZONE PM manual).
AUTO raises :class:WatlowConfigurationError — there is no
single factory framing for AUTO, the detector probes both.
Callers crossing protocol boundaries (the maintenance helpers
that switch protocol, watlow-discover --protocol both)
should rebuild settings per protocol via this method instead
of inheriting whatever framing the previous call used.
Source code in src/watlowlib/transport/base.py
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware should use
:class:watlowlib.transport.fake.FakeTransport; the two conform to
the same structural :class:Transport Protocol.
Source code in src/watlowlib/transport/serial.py
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry,
implementations raise :class:watlowlib.errors.WatlowTimeoutError
— never return an empty or partial bytes silently. Backend
exceptions normalise to
:class:watlowlib.errors.WatlowTransportError (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 and ProtocolKind.AUTO probe gaps.
Source code in src/watlowlib/transport/base.py
read_exact
async
¶
Read exactly n bytes.
Raises :class:watlowlib.errors.WatlowTimeoutError if fewer
than n bytes arrive before timeout. Partial buffers are
retained for the next call — implementations must not discard
them.
Source code in src/watlowlib/transport/base.py
Base Protocol + serial settings¶
watlowlib.transport.base ¶
Transport :pep:544 Protocol + :class:SerialSettings.
The transport surface is intentionally small — Standard Bus is fully
length-prefixed (BACnet MS/TP outer frame), so the protocol client
only needs write and read_exact. read_available exists for
draining the line between auto-detect probes; drain_input is the
synchronous flush used after a framing error before the next attempt.
Default serial framing for Standard Bus on the EZ-ZONE PM family is
38400 8-N-1 per the PM manuals; Modbus RTU on the same family is
configurable across 9600 / 19200 / 38400 / 57600 / 115200. The
:class:SerialSettings defaults match the Std Bus factory state.
SerialSettings
dataclass
¶
SerialSettings(
port,
baudrate=38400,
bytesize=ByteSize.EIGHT,
parity=Parity.NONE,
stopbits=StopBits.ONE,
rtscts=False,
xonxoff=False,
exclusive=True,
)
Serial-port configuration for :class:SerialTransport.
Mirrors :class:anyserial.SerialConfig plus a port path. Default
framing is 38400 8-N-1, the EZ-ZONE PM Standard Bus factory
setting. exclusive defaults True because Standard Bus is
poll/response and won't tolerate a second writer.
The __post_init__ accepts int / float / str shorthand
at runtime for the framing fields (bytesize=8, parity="none",
stopbits=1) and normalises to the enum. The static field types
are the enums themselves so mypy --strict users must pass
:class:anyserial.ByteSize / :class:anyserial.Parity /
:class:anyserial.StopBits directly; the runtime shorthand is
primarily for CLI argument parsing and interactive scripts.
factory_for
classmethod
¶
Return the EZ-ZONE PM factory framing for protocol.
STDBUS→ 38400 8-N-1 (the Standard Bus factory default).MODBUS_RTU→ 9600 8-E-1 (the Modbus RTU factory default per the EZ-ZONE PM manual).
AUTO raises :class:WatlowConfigurationError — there is no
single factory framing for AUTO, the detector probes both.
Callers crossing protocol boundaries (the maintenance helpers
that switch protocol, watlow-discover --protocol both)
should rebuild settings per protocol via this method instead
of inheriting whatever framing the previous call used.
Source code in src/watlowlib/transport/base.py
Transport ¶
Bases: Protocol
Byte-level transport.
Every I/O boundary takes an explicit timeout. On expiry,
implementations raise :class:watlowlib.errors.WatlowTimeoutError
— never return an empty or partial bytes silently. Backend
exceptions normalise to
:class:watlowlib.errors.WatlowTransportError (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 and ProtocolKind.AUTO probe gaps.
Source code in src/watlowlib/transport/base.py
read_exact
async
¶
Read exactly n bytes.
Raises :class:watlowlib.errors.WatlowTimeoutError if fewer
than n bytes arrive before timeout. Partial buffers are
retained for the next call — implementations must not discard
them.
Source code in src/watlowlib/transport/base.py
Serial transport¶
watlowlib.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
normalise to :mod:watlowlib.errors types with __cause__ preserved.
SerialTransport ¶
:class:Transport backed by a real serial port via anyserial.
Tests that don't need hardware should use
:class:watlowlib.transport.fake.FakeTransport; the two conform to
the same structural :class:Transport Protocol.
Source code in src/watlowlib/transport/serial.py
Fake transport¶
watlowlib.transport.fake ¶
In-process fake transport for tests and fixture replay.
:class:FakeTransport implements the :class:Transport Protocol
without touching a serial port. Tests script the expected
write→response mapping; unscripted writes are recorded but produce no
reply, which surfaces as a real timeout on the next read (the intended
failure mode — tests notice when they forgot to script a command).
The transport is fixture-replay grade:
- The dict-based
scriptmatches by exact bytes. - An optional ordered
queueof(write, reply)pairs is consumed FIFO and is the right shape for capture-replay scenarios where the same request may legitimately appear more than once with a different reply (a recorder reading PV in a tight loop, say). - :attr:
unmatched_writesexposes the subset of :attr:writesthat hit neither a dict entry nor the next queue entry — useful for tests that want to assert "no surprise traffic hit the wire".
FakeSlave ¶
Scripted :class:anymodbus.Slave stand-in for tests.
Mirrors the surface :class:watlowlib.protocol.modbus.client.ModbusProtocolClient
actually calls (read_holding_registers,
read_input_registers, write_register, write_registers)
and records every call. Tests assert on :attr:reads and
:attr:writes to verify the :class:ModbusOp lowered correctly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[tuple[str, int], ScriptedSlaveEntry] | None
|
|
None
|
Source code in src/watlowlib/transport/fake.py
add_script ¶
Register or overwrite a scripted reply for (method, address).
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/watlowlib/transport/fake.py
unmatched_writes
property
¶
Writes that didn't match any scripted reply, in order.
A test can assert transport.unmatched_writes == () to catch
accidentally-unscripted traffic — the corresponding read would
have timed out, but a precise assertion fails faster and points
at the right call.
add_script ¶
extend_queue ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that left chatter on the line which the protocol client has to drain on recovery.