watlowlib.devices¶
The Controller facade, Session, typed dataclasses (Reading,
DeviceInfo, PartNumber, AlarmState, LoopState,
DiscoveryResult, DeviceSnapshot, WatlowDeviceSnapshot, …),
ControllerFamily, Capability, SafetyTier, Availability,
open_device, and find_devices. See Controllers.
Public surface¶
watlowlib.devices ¶
Device facade — :class:Controller, :class:Session, and dataclasses.
The facade is the public surface; everything else
(:mod:watlowlib.protocol, :mod:watlowlib.commands,
:mod:watlowlib.registry) is implementation detail callers don't
have to import.
AlarmState
dataclass
¶
Decoded alarm bits for one loop.
Availability ¶
Bases: StrEnum
Per-command session state.
Sticky for the session: once a command transitions to
:attr:UNSUPPORTED, the session short-circuits subsequent
invocations with a typed error pre-I/O. The transition table
lives in docs/design.md §5b.
Capability ¶
Bases: Flag
Coarse hardware capability bits.
Bits are derived from a decoded part number when one is available
(see :func:watlowlib.registry.families.capabilities_for_part_number)
and fall back to a per-family prior otherwise. The session widens
the set at runtime when a command succeeds against a parameter
that proves the capability.
The vocabulary is small on purpose — most Watlow gating is by
:class:watlowlib.registry.families.ControllerFamily and by
:attr:watlowlib.registry.parameters.ParameterSpec.parameter_id,
not by per-feature bits. New bits are added when captured family
behaviour requires them.
Controller ¶
Async facade for a single Watlow controller.
Source code in src/watlowlib/devices/controller.py
capabilities
property
¶
Cached SKU capabilities (set after :meth:identify).
None pre-identify so capability-gated operations behave
permissively until the part number is captured. After
:meth:identify, callers can branch on
:attr:Capability.HAS_COOLING etc. without re-issuing
identify.
loops
property
¶
Cached loop count (set after :meth:identify).
None until the device's part number has been decoded;
:meth:loop accepts any 1-indexed value while loops is
None and falls back to per-spec validation at the first
wire call. After :meth:identify, loops reflects the
decoded value.
serial_settings
property
¶
Serial framing the controller was opened with.
Exposed so an identity strategy (see
:mod:watlowlib.devices.profile) can stamp it onto the
:class:DeviceInfo it builds.
close
async
¶
Close the underlying transport and dispose the protocol client.
Source code in src/watlowlib/devices/controller.py
identify
async
¶
Read the identity parameters and return a :class:DeviceInfo.
Reads (in order): part number (1009), hardware id (1001),
firmware id (1002), serial number. Missing secondary fields
stay None and the result's :attr:DeviceInfo.health is
promoted from :attr:DeviceHealth.OK to
:attr:DeviceHealth.PARTIAL. If the part-number read itself
fails, the result's health is :attr:DeviceHealth.FAILED and
capability decoding is skipped (the family prior still
applies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float | None
|
Per-read timeout override. |
None
|
strict
|
bool
|
If |
False
|
query_configured_protocol
|
bool
|
If |
False
|
Raises:
| Type | Description |
|---|---|
WatlowError
|
When |
Source code in src/watlowlib/devices/controller.py
loop ¶
Return a sub-facade bound to loop n (1-indexed).
n is validated eagerly when :attr:loops is known,
otherwise per-spec max_instance validation kicks in at the
first wire call. Multi-loop access is the public way to reach
loop 2 on dual-loop devices —
:meth:Controller.read_pv defaults to instance=1.
Source code in src/watlowlib/devices/controller.py
poll
async
¶
Read the active process value — the canonical no-arg snapshot.
Equivalent to :meth:read_pv. The no-arg form aligns with the
ecosystem poll() convention shared by alicatlib.Device,
sartoriuslib.Balance, and nidaqlib.DaqSession: a
single, default-shaped reading per call.
For multi-parameter polling use :meth:poll_many.
Source code in src/watlowlib/devices/controller.py
poll_many
async
¶
Read every (parameter × instance) and return them as :class:Sample\ s.
Satisfies the :class:watlowlib.streaming.PollSource Protocol so
a solo :class:Controller can drive :func:watlowlib.streaming.record
directly without a manager. names is accepted for Protocol
compatibility but ignored — a Controller has only one device.
Failed reads are dropped from the returned list and logged at WARN. The recorder treats absence as "drop this row from the batch" and continues with the next tick.
Source code in src/watlowlib/devices/controller.py
read_comms_unit_label
async
¶
Read (and cache) the value parameter 17050 reports.
Inspection / diagnostics helper. Does not drive
:class:Reading.unit: on at least one PM3 firmware revision
17050 is a label-only register that changes the enum the
device reports for itself but does not affect the scale of
temperature values exchanged over comms.
To tell watlowlib what scale temperatures actually travel in
over the wire, pass assert_wire_temperature_unit= to
:func:watlowlib.open_device. That assertion is what feeds
:class:Reading.unit.
Distinct from read_parameter("units"), which targets
parameter 3005 (front-panel display). The two can disagree on
a real device.
Returns None if the device doesn't report a known code.
Source code in src/watlowlib/devices/controller.py
read_parameter
async
¶
Read any registry parameter.
instance=1 is the default for single-loop devices and the
first loop / channel on multi-loop devices.
Source code in src/watlowlib/devices/controller.py
read_pv
async
¶
Read the process value for instance (loop number, 1-indexed).
Source code in src/watlowlib/devices/controller.py
read_setpoint
async
¶
Read the active setpoint for instance.
Source code in src/watlowlib/devices/controller.py
set_comms_unit_label
async
¶
Set parameter 17050 ("Communications - Display Units").
Accepts a :class:Unit or a case-insensitive string alias
("C" / "F" / "celsius" / "fahrenheit" /
"degC" / "degF" / "°C" / "°F").
:attr:Unit.PERCENT is rejected pre-I/O — the register is
temperature-only.
Raw enumeration codes (15 / 30) are not accepted here. Callers
who want the lower-level path use
write_parameter("display_units", 30).
.. warning::
On at least one PM3 firmware revision this register is
**label-only**: writing it changes the enum the device
reports when 17050 is read back, but does not change the
scale of temperature values exchanged over comms. This
setter therefore does **not** affect
:class:`Reading.unit`. To tell watlowlib what scale
temperatures are actually on, pass
``assert_wire_temperature_unit=`` to
:func:`watlowlib.open_device`.
Persistent write (parameter 17050 is RWE); pass confirm=True
to acknowledge the EEPROM write. The session raises
:class:WatlowConfirmationRequiredError pre-I/O if missing.
Returns the device-echoed label after the write. None if
the device's echo decodes outside the known codes.
Source code in src/watlowlib/devices/controller.py
set_persistent_writes
async
¶
Toggle whether subsequent writes persist to non-volatile memory.
Series-SD-specific. The SD persists every register write to
EEPROM by default, so a high-rate writer (ramping setpoints, a
tuning loop) can wear the EEPROM out and brick the controller.
Writing 0 to register 17 keeps subsequent writes in RAM
only; the device resets register 17 to 1 on every power
cycle, so call set_persistent_writes(False) once after each
power-up before a burst of writes (see sd_manual.txt p.84).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
enabled
|
bool
|
|
required |
confirm
|
bool
|
The write itself is gated like any other parameter
write — pass |
False
|
timeout
|
float | None
|
Per-write timeout override. |
None
|
Raises:
| Type | Description |
|---|---|
WatlowConfirmationRequiredError
|
|
WatlowValidationError
|
the bound profile's registry has no
|
Source code in src/watlowlib/devices/controller.py
set_setpoint
async
¶
Write the setpoint and return the device-echoed value as a :class:Reading.
Setpoint is RWES — pass confirm=True to acknowledge the
EEPROM write. The returned reading is the device's echo of
the value it accepted.
Source code in src/watlowlib/devices/controller.py
snapshot
async
¶
Return an I/O-free :class:WatlowDeviceSnapshot.
Built from cached identity (populated by :meth:identify,
which :func:watlowlib.open_device calls by default) plus
the session's last error and per-command availability cache.
Does not issue any reads — safe to call from monitoring
loops at high cadence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Override the snapshot's |
None
|
Source code in src/watlowlib/devices/controller.py
write_parameter
async
¶
Write any registry parameter.
Persistent (RWE / RWES) writes require confirm=True;
the session raises :class:WatlowConfirmationRequiredError
before any I/O if the gate is missing.
Source code in src/watlowlib/devices/controller.py
ControllerFamily ¶
Bases: StrEnum
Watlow controller family discriminator.
Membership here is advisory — :class:watlowlib.devices.session.Session
treats family hints as priors, not gates. See docs/design.md §5b.
ControllerLoop ¶
A view over one control loop on a :class:Controller.
Construct via :meth:Controller.loop; never instantiated
directly by user code. The sub-facade lives only as long as the
parent controller's session — closing the controller is the only
cleanup needed.
Source code in src/watlowlib/devices/loop.py
read_alarms
async
¶
Read the alarm word for this loop.
Currently raises :class:watlowlib.errors.WatlowProtocolUnsupportedError —
see :func:watlowlib.commands.alarms.read_alarms for why the
decoder is not yet wired up.
Source code in src/watlowlib/devices/loop.py
read_output
async
¶
read_pid
async
¶
Read every PID gain for this loop. Missing gains return None.
Cool-side gains (cool_proportional_band, dead_band)
are skipped when the controller's identified capabilities
lack :attr:Capability.HAS_COOLING (e.g. PM output_2 ==
'A'). Pre-identify, the gate is permissive.
Source code in src/watlowlib/devices/loop.py
read_pv
async
¶
read_setpoint
async
¶
set_setpoint
async
¶
Write this loop's setpoint (RWES → confirm=True required).
Source code in src/watlowlib/devices/loop.py
write_pid
async
¶
Write the supplied gains for this loop.
Persistent — passing confirm=True is required. Fields
left None on gains skip the wire entirely. Setting a
cool-side field on a controller without
:attr:Capability.HAS_COOLING raises
:class:watlowlib.errors.WatlowConfigurationError.
Source code in src/watlowlib/devices/loop.py
DeviceInfo
dataclass
¶
DeviceInfo(
part_number,
hardware_id,
firmware_id,
serial_number,
family,
protocol,
address,
capabilities,
serial_settings,
loops,
health=DeviceHealth.OK,
configured_protocol=None,
)
Identity + connection metadata for an open controller.
Returned by :meth:Controller.identify. Capabilities are decoded
from the part number when one is captured (see
:func:watlowlib.registry.families.capabilities_for_part_number)
and OR-ed with the family prior; unobserved bits stay zero rather
than being guessed.
protocol is the wire protocol the host is currently talking;
configured_protocol is what the device's persistent EEPROM
parameter (PM 17009) reports. They normally match, but when they
diverge the helper :attr:protocol_mismatch flags it — useful
for catching SKU/firmware combinations where the user wrote a new
protocol but the runtime stack didn't pick it up (e.g. comms
position-8 = 'A', no Modbus stack present even though 17009 reads
1057).
protocol_mismatch
property
¶
True when EEPROM says one protocol and we're talking another.
Always False when :attr:configured_protocol is None
(i.e. identify did not query parameter 17009).
DeviceProfile
dataclass
¶
DeviceProfile(
name,
family,
registry,
default_protocol,
default_serial,
identify,
wire_temperature_unit=None,
)
A first-class controller type.
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Stable short identifier ( |
family |
ControllerFamily
|
The controller family this profile describes. |
registry |
ParameterRegistry
|
Parameter registry used to decode this device's parameters. |
default_protocol |
ProtocolKind
|
Wire protocol opened when the caller does not pass one explicitly. |
default_serial |
SerialSettings
|
Factory serial framing for |
identify |
IdentifyStrategy
|
Strategy that produces a :class: |
wire_temperature_unit |
Unit | None
|
The scale temperatures travel in over the
wire, when the profile knows it for certain. |
DeviceSnapshot
dataclass
¶
DeviceSnapshot(
name,
model,
firmware,
serial,
connected,
last_error,
recoverable_error_count,
captured_at,
)
Cross-library identity + connection summary (no I/O).
Attributes:
| Name | Type | Description |
|---|---|---|
name |
str
|
Caller-supplied device name (manager-assigned, or the transport label for a solo controller). |
model |
str | None
|
Best-known model / part-number string, |
firmware |
str | None
|
Firmware id as a string, or |
serial |
str | None
|
Serial-number string, or |
connected |
bool
|
|
last_error |
ErrorContext | None
|
Most recent :class: |
recoverable_error_count |
int
|
Session counter for swallowed-and- retried transient errors. Watlow keeps this dormant until a transient transport class is introduced; the field stays at zero today. |
captured_at |
datetime
|
Wall-clock at snapshot construction (tz-aware UTC). |
DiscoveryResult
dataclass
¶
One probe attempt's outcome from :func:find_devices.
Cross-library shape (mirrors :mod:alicatlib, :mod:sartoriuslib,
:mod:nidaqlib) so GUI Discover dialogs and capa-style
adapters can filter responsive vs silent rows on a single
attribute and consume the same field set across vendors.
A populated :attr:device_info carries the full
:class:Controller.identify result, including
:attr:DeviceInfo.health and (when the scan queried it)
:attr:DeviceInfo.configured_protocol.
The address field is typed str | int | None to match the
cross-library spec; in practice every watlow probe carries an
int.
IdentifyStrategy ¶
Bases: Protocol
How a profile turns an open controller into a :class:DeviceInfo.
Implementations are pure with respect to the controller's cached
identity — they read parameters and return a :class:DeviceInfo;
:meth:Controller.identify is responsible for caching the result.
__call__
async
¶
Return identity information for controller.
LoopState
dataclass
¶
Snapshot of one loop. Composed from several reads.
ParameterEntry
dataclass
¶
Generic registry-driven read/write result.
Returned by :data:watlowlib.commands.READ_PARAMETER and
:data:watlowlib.commands.WRITE_PARAMETER. The
:class:Controller translates an entry into a :class:Reading /
:class:PartNumber / etc. when the public API guarantees a
richer shape.
PartNumber
dataclass
¶
Parsed part-number string returned by read_part_number.
Per-family digit decoding is contributed by
:mod:watlowlib.registry.families. Decoded fragments live in
:attr:details as a free-form mapping so each family can populate
only what its ordering format defines, and so adding fragments to
the PM decoder later is non-breaking.
The EZ-ZONE PM decoder populates case size, control type, power
input, three output codes, and options string. Other families fall
through to a stub: only :attr:family is set, and :attr:details
is empty.
Reading
dataclass
¶
A single timestamped value from the controller.
protocol is set by the variant decoder, not by the facade —
it reflects which wire protocol produced the value (per
docs/design.md invariant 7).
SafetyTier ¶
Bases: IntEnum
How dangerous a command is to invoke.
READ_ONLY(R) — no state change.STATEFUL— runtime state change but not EEPROM-backed. Reserved for commands like "start autotune"; no PM parameter maps here today, but the tier exists so future commands have a place to live.PERSISTENT(RW / RWE / RWES) — EEPROM-backed; requiresconfirm=Trueat the facade.
Session ¶
Owns availability cache, gates, and the dispatch loop.
A :class:Session is bound to exactly one :class:ProtocolClient
for its lifetime — one protocol per port (invariant 1).
Source code in src/watlowlib/devices/session.py
client
property
¶
The bound protocol client.
Exposed for the watlow-raw escape hatch and for diagnostics
that need to issue an unframed wire op outside the registry.
Callers must acquire :attr:ProtocolClient.lock before
:meth:ProtocolClient.execute to honour the per-port
serialization invariant, and must pass this session's
:attr:address (or another concrete address for multi-drop
diagnostics) to execute.
registry
property
¶
Parameter registry bound to this session.
Exposed for the streaming layer so polling code can resolve a
name / id to a :class:ParameterSpec without an extra import
of the module-level :data:PARAMETERS.
availability ¶
availability_summary ¶
Return the current command availability cache.
Includes every command the session has dispatched; the snapshot path filters to the UNSUPPORTED entries.
Source code in src/watlowlib/devices/session.py
clear_last_error ¶
comms_unit_label
async
¶
Return the value parameter 17050 reports for this session.
Inspection helper, not a source of truth: on at least one PM3
firmware revision 17050 is a label-only register that does not
govern the wire scale. :class:Reading.unit is sourced from
:meth:wire_temperature_unit instead.
Reads the parameter on first call and caches the result for
the lifetime of the session. Returns None when the device
rejects the read or reports an unknown code; the cache
distinguishes "haven't asked yet" from "asked and got nothing"
so a rejection does not cost another wire turn-around.
Invalidated by :meth:invalidate_comms_unit_label (called by
:meth:Controller.set_comms_unit_label after a successful
write).
Source code in src/watlowlib/devices/session.py
dispose ¶
execute
async
¶
Dispatch command with request and return the typed response.
Source code in src/watlowlib/devices/session.py
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | |
invalidate_comms_unit_label ¶
Drop the cached 17050 value so the next read re-queries the device.
set_wire_temperature_unit ¶
Set the wire temperature scale from an authoritative source.
Called by an identity strategy that has read the device's own unit register (e.g. the Series SD reg 18). Unlike the PM path — where the unit register can lie, so the value stays a user assertion — this is the device telling us its comms scale directly, so it is honest to adopt it. Resets the one-shot "trusting user-asserted unit" warning since this value is device-sourced, not user-asserted.
Source code in src/watlowlib/devices/session.py
wire_temperature_unit ¶
Return the user-asserted scale of temperature values on the wire.
This is what :class:Reading.unit / :class:Sample.unit get
tagged with for temperature parameters. None when the user
did not pass assert_wire_temperature_unit= to
:func:watlowlib.open_device; in that case temperature
readings carry unit=None rather than guess.
Pure accessor — no I/O. Logs a one-shot WARN the first time an asserted value is consumed so the user-assertion shows up plainly in capture logs.
Source code in src/watlowlib/devices/session.py
WatlowDeviceSnapshot
dataclass
¶
WatlowDeviceSnapshot(
name,
model,
firmware,
serial,
connected,
last_error,
recoverable_error_count,
captured_at,
family,
capabilities,
availability_summary=(lambda: {})(),
)
Bases: DeviceSnapshot
Watlow-specific extension of :class:DeviceSnapshot.
Attributes:
| Name | Type | Description |
|---|---|---|
family |
ControllerFamily | None
|
Decoded :class: |
capabilities |
Capability
|
SKU-decoded :class: |
availability_summary |
Mapping[str, Availability]
|
Frozen mapping of command names that
the session has marked :attr: |
classify_family ¶
Return the :class:ControllerFamily for a part-number string.
Only the leading family discriminator is parsed; per-family digit
decoding is in :func:decode_part_number.
Source code in src/watlowlib/registry/families.py
find_devices
async
¶
find_devices(
*,
ports=None,
addresses=None,
baudrates=None,
protocols=None,
profiles=None,
serial_template=None,
per_probe_timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Probe local serial ports for Watlow controllers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ports
|
Sequence[str] | None
|
Serial-port paths to scan. |
None
|
addresses
|
Sequence[int] | None
|
Bus addresses to probe per (port, baudrate, protocol)
combination. Defaults to :data: |
None
|
baudrates
|
Sequence[int] | None
|
Baud rates to try. Defaults to
:data: |
None
|
protocols
|
Sequence[ProtocolKind] | None
|
Wire protocols to probe. Defaults to
:data: |
None
|
profiles
|
Sequence[DeviceProfile] | None
|
Device profiles to probe. When given, discovery
iterates profiles instead of |
None
|
serial_template
|
SerialSettings | None
|
Optional :class: |
None
|
per_probe_timeout_s
|
float
|
Per-probe budget. Bounds the
:meth: |
_DEFAULT_PROBE_TIMEOUT_S
|
Returns:
| Name | Type | Description |
|---|---|---|
One |
list[DiscoveryResult]
|
class: |
list[DiscoveryResult]
|
address) tuple, in input order. The cartesian product is |
|
list[DiscoveryResult]
|
iterated outermost-port, then baudrate, then protocol, then |
|
list[DiscoveryResult]
|
address — same input → same output ordering. |
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
Notes
- Read-only. Discovery never writes to the device; it
only calls :meth:
Controller.identify(four parameter reads). Safe to run on rigs that already have other software talking to the controller. - Per-port short-circuit. If a port fails to open with a
:class:
WatlowConnectionError, the rest of the scan for that port emitsok=Falserows without re-attempting the open. This avoids hammering a port the kernel won't give us.
Source code in src/watlowlib/devices/discovery.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |
open_device
async
¶
open_device(
port,
*,
profile=EZZONE_PROFILE,
protocol=None,
address=1,
serial_settings=None,
assert_wire_temperature_unit=None,
identify=True,
)
Open a controller on a serial port.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str
|
Serial-port path ( |
required |
profile
|
DeviceProfile
|
The device profile to open against. Defaults to
:data: |
EZZONE_PROFILE
|
protocol
|
ProtocolKind | None
|
Wire protocol. |
None
|
address
|
int
|
Bus address. Std Bus accepts |
1
|
serial_settings
|
SerialSettings | None
|
Optional framing override. |
None
|
identify
|
bool
|
When |
True
|
assert_wire_temperature_unit
|
Unit | str | None
|
User-asserted scale of
temperature values on the wire. Sets
:class: |
None
|
Returns:
| Type | Description |
|---|---|
Controller
|
An opened :class: |
Controller
|
meth: |
Controller
|
Every protocol ( |
Controller
|
an opened controller; |
Controller
|
|
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
WatlowValidationError
|
|
WatlowProtocolUnsupportedError
|
|
Source code in src/watlowlib/devices/factory.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |
Controller¶
watlowlib.devices.controller ¶
The :class:Controller facade — public API for one device.
Single-device surface:
- :meth:
identify - :meth:
read_pv/ :meth:read_setpoint/ :meth:set_setpoint - :meth:
read_parameter/ :meth:write_parameter - :meth:
loop(multi-loop access), PID, alarms
Lifecycle is async-context-manager: async with await open_device(...)
opens the transport on __aenter__ and disposes the protocol client
+ closes the transport on __aexit__.
Controller ¶
Async facade for a single Watlow controller.
Source code in src/watlowlib/devices/controller.py
capabilities
property
¶
Cached SKU capabilities (set after :meth:identify).
None pre-identify so capability-gated operations behave
permissively until the part number is captured. After
:meth:identify, callers can branch on
:attr:Capability.HAS_COOLING etc. without re-issuing
identify.
loops
property
¶
Cached loop count (set after :meth:identify).
None until the device's part number has been decoded;
:meth:loop accepts any 1-indexed value while loops is
None and falls back to per-spec validation at the first
wire call. After :meth:identify, loops reflects the
decoded value.
serial_settings
property
¶
Serial framing the controller was opened with.
Exposed so an identity strategy (see
:mod:watlowlib.devices.profile) can stamp it onto the
:class:DeviceInfo it builds.
close
async
¶
Close the underlying transport and dispose the protocol client.
Source code in src/watlowlib/devices/controller.py
identify
async
¶
Read the identity parameters and return a :class:DeviceInfo.
Reads (in order): part number (1009), hardware id (1001),
firmware id (1002), serial number. Missing secondary fields
stay None and the result's :attr:DeviceInfo.health is
promoted from :attr:DeviceHealth.OK to
:attr:DeviceHealth.PARTIAL. If the part-number read itself
fails, the result's health is :attr:DeviceHealth.FAILED and
capability decoding is skipped (the family prior still
applies).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
timeout
|
float | None
|
Per-read timeout override. |
None
|
strict
|
bool
|
If |
False
|
query_configured_protocol
|
bool
|
If |
False
|
Raises:
| Type | Description |
|---|---|
WatlowError
|
When |
Source code in src/watlowlib/devices/controller.py
loop ¶
Return a sub-facade bound to loop n (1-indexed).
n is validated eagerly when :attr:loops is known,
otherwise per-spec max_instance validation kicks in at the
first wire call. Multi-loop access is the public way to reach
loop 2 on dual-loop devices —
:meth:Controller.read_pv defaults to instance=1.
Source code in src/watlowlib/devices/controller.py
poll
async
¶
Read the active process value — the canonical no-arg snapshot.
Equivalent to :meth:read_pv. The no-arg form aligns with the
ecosystem poll() convention shared by alicatlib.Device,
sartoriuslib.Balance, and nidaqlib.DaqSession: a
single, default-shaped reading per call.
For multi-parameter polling use :meth:poll_many.
Source code in src/watlowlib/devices/controller.py
poll_many
async
¶
Read every (parameter × instance) and return them as :class:Sample\ s.
Satisfies the :class:watlowlib.streaming.PollSource Protocol so
a solo :class:Controller can drive :func:watlowlib.streaming.record
directly without a manager. names is accepted for Protocol
compatibility but ignored — a Controller has only one device.
Failed reads are dropped from the returned list and logged at WARN. The recorder treats absence as "drop this row from the batch" and continues with the next tick.
Source code in src/watlowlib/devices/controller.py
read_comms_unit_label
async
¶
Read (and cache) the value parameter 17050 reports.
Inspection / diagnostics helper. Does not drive
:class:Reading.unit: on at least one PM3 firmware revision
17050 is a label-only register that changes the enum the
device reports for itself but does not affect the scale of
temperature values exchanged over comms.
To tell watlowlib what scale temperatures actually travel in
over the wire, pass assert_wire_temperature_unit= to
:func:watlowlib.open_device. That assertion is what feeds
:class:Reading.unit.
Distinct from read_parameter("units"), which targets
parameter 3005 (front-panel display). The two can disagree on
a real device.
Returns None if the device doesn't report a known code.
Source code in src/watlowlib/devices/controller.py
read_parameter
async
¶
Read any registry parameter.
instance=1 is the default for single-loop devices and the
first loop / channel on multi-loop devices.
Source code in src/watlowlib/devices/controller.py
read_pv
async
¶
Read the process value for instance (loop number, 1-indexed).
Source code in src/watlowlib/devices/controller.py
read_setpoint
async
¶
Read the active setpoint for instance.
Source code in src/watlowlib/devices/controller.py
set_comms_unit_label
async
¶
Set parameter 17050 ("Communications - Display Units").
Accepts a :class:Unit or a case-insensitive string alias
("C" / "F" / "celsius" / "fahrenheit" /
"degC" / "degF" / "°C" / "°F").
:attr:Unit.PERCENT is rejected pre-I/O — the register is
temperature-only.
Raw enumeration codes (15 / 30) are not accepted here. Callers
who want the lower-level path use
write_parameter("display_units", 30).
.. warning::
On at least one PM3 firmware revision this register is
**label-only**: writing it changes the enum the device
reports when 17050 is read back, but does not change the
scale of temperature values exchanged over comms. This
setter therefore does **not** affect
:class:`Reading.unit`. To tell watlowlib what scale
temperatures are actually on, pass
``assert_wire_temperature_unit=`` to
:func:`watlowlib.open_device`.
Persistent write (parameter 17050 is RWE); pass confirm=True
to acknowledge the EEPROM write. The session raises
:class:WatlowConfirmationRequiredError pre-I/O if missing.
Returns the device-echoed label after the write. None if
the device's echo decodes outside the known codes.
Source code in src/watlowlib/devices/controller.py
set_persistent_writes
async
¶
Toggle whether subsequent writes persist to non-volatile memory.
Series-SD-specific. The SD persists every register write to
EEPROM by default, so a high-rate writer (ramping setpoints, a
tuning loop) can wear the EEPROM out and brick the controller.
Writing 0 to register 17 keeps subsequent writes in RAM
only; the device resets register 17 to 1 on every power
cycle, so call set_persistent_writes(False) once after each
power-up before a burst of writes (see sd_manual.txt p.84).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
enabled
|
bool
|
|
required |
confirm
|
bool
|
The write itself is gated like any other parameter
write — pass |
False
|
timeout
|
float | None
|
Per-write timeout override. |
None
|
Raises:
| Type | Description |
|---|---|
WatlowConfirmationRequiredError
|
|
WatlowValidationError
|
the bound profile's registry has no
|
Source code in src/watlowlib/devices/controller.py
set_setpoint
async
¶
Write the setpoint and return the device-echoed value as a :class:Reading.
Setpoint is RWES — pass confirm=True to acknowledge the
EEPROM write. The returned reading is the device's echo of
the value it accepted.
Source code in src/watlowlib/devices/controller.py
snapshot
async
¶
Return an I/O-free :class:WatlowDeviceSnapshot.
Built from cached identity (populated by :meth:identify,
which :func:watlowlib.open_device calls by default) plus
the session's last error and per-command availability cache.
Does not issue any reads — safe to call from monitoring
loops at high cadence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str | None
|
Override the snapshot's |
None
|
Source code in src/watlowlib/devices/controller.py
write_parameter
async
¶
Write any registry parameter.
Persistent (RWE / RWES) writes require confirm=True;
the session raises :class:WatlowConfirmationRequiredError
before any I/O if the gate is missing.
Source code in src/watlowlib/devices/controller.py
Session¶
watlowlib.devices.session ¶
The :class:Session — single dispatch point for every command.
The session is the only place that gates, logs, and updates
:class:Availability. Variants are pure (ctx, request) → response
functions; protocol clients only own the wire. Per docs/design.md
invariant 2, no other layer touches these concerns.
Responsibilities (in order, per execute):
- Resolve the protocol variant.
UNSUPPORTEDis sticky — short- circuit pre-I/O on a typed error. - Enforce
confirm=Truefor :attr:SafetyTier.PERSISTENTwrites. - Acquire the per-port lock on the protocol client.
- Variant
encode→client.execute→ variantdecode. - Map success / typed errors to availability transitions and log a structured event.
Variant signatures differ across protocols (see
docs/design.md §5):
- Std Bus variants take
decode(reply, ctx)— the reply already carries the parameter selector echoed by the device. - Modbus variants take
decode(words, ctx, request)— the wire carries no echo, so the variant re-resolves the spec from the request to interpret the words.
Session ¶
Owns availability cache, gates, and the dispatch loop.
A :class:Session is bound to exactly one :class:ProtocolClient
for its lifetime — one protocol per port (invariant 1).
Source code in src/watlowlib/devices/session.py
client
property
¶
The bound protocol client.
Exposed for the watlow-raw escape hatch and for diagnostics
that need to issue an unframed wire op outside the registry.
Callers must acquire :attr:ProtocolClient.lock before
:meth:ProtocolClient.execute to honour the per-port
serialization invariant, and must pass this session's
:attr:address (or another concrete address for multi-drop
diagnostics) to execute.
registry
property
¶
Parameter registry bound to this session.
Exposed for the streaming layer so polling code can resolve a
name / id to a :class:ParameterSpec without an extra import
of the module-level :data:PARAMETERS.
availability ¶
availability_summary ¶
Return the current command availability cache.
Includes every command the session has dispatched; the snapshot path filters to the UNSUPPORTED entries.
Source code in src/watlowlib/devices/session.py
clear_last_error ¶
comms_unit_label
async
¶
Return the value parameter 17050 reports for this session.
Inspection helper, not a source of truth: on at least one PM3
firmware revision 17050 is a label-only register that does not
govern the wire scale. :class:Reading.unit is sourced from
:meth:wire_temperature_unit instead.
Reads the parameter on first call and caches the result for
the lifetime of the session. Returns None when the device
rejects the read or reports an unknown code; the cache
distinguishes "haven't asked yet" from "asked and got nothing"
so a rejection does not cost another wire turn-around.
Invalidated by :meth:invalidate_comms_unit_label (called by
:meth:Controller.set_comms_unit_label after a successful
write).
Source code in src/watlowlib/devices/session.py
dispose ¶
execute
async
¶
Dispatch command with request and return the typed response.
Source code in src/watlowlib/devices/session.py
208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 | |
invalidate_comms_unit_label ¶
Drop the cached 17050 value so the next read re-queries the device.
set_wire_temperature_unit ¶
Set the wire temperature scale from an authoritative source.
Called by an identity strategy that has read the device's own unit register (e.g. the Series SD reg 18). Unlike the PM path — where the unit register can lie, so the value stays a user assertion — this is the device telling us its comms scale directly, so it is honest to adopt it. Resets the one-shot "trusting user-asserted unit" warning since this value is device-sourced, not user-asserted.
Source code in src/watlowlib/devices/session.py
wire_temperature_unit ¶
Return the user-asserted scale of temperature values on the wire.
This is what :class:Reading.unit / :class:Sample.unit get
tagged with for temperature parameters. None when the user
did not pass assert_wire_temperature_unit= to
:func:watlowlib.open_device; in that case temperature
readings carry unit=None rather than guess.
Pure accessor — no I/O. Logs a one-shot WARN the first time an asserted value is consumed so the user-assertion shows up plainly in capture logs.
Source code in src/watlowlib/devices/session.py
Loops¶
watlowlib.devices.loop ¶
Per-loop sub-facade returned by :meth:Controller.loop.
A :class:ControllerLoop is a thin view over a :class:Controller
that pre-binds an instance argument. It validates the loop number
once at construction (cross-cutting invariant 6: 1-indexed everywhere)
and forwards every operation to the parent controller's session,
threading the loop index as the registry instance.
The sub-facade is stateless beyond the loop number — it does not
duplicate the controller's transport, lock, or availability cache.
Multiple :class:ControllerLoop instances over the same controller
share the underlying session safely; concurrent calls serialize on
the protocol client's lock.
This module intentionally has no protocol-specific code. PID,
output, and alarm helpers live in :mod:watlowlib.commands.loop and
:mod:watlowlib.commands.alarms so the facade-only logic and the
parameter aggregation logic stay separate.
ControllerLoop ¶
A view over one control loop on a :class:Controller.
Construct via :meth:Controller.loop; never instantiated
directly by user code. The sub-facade lives only as long as the
parent controller's session — closing the controller is the only
cleanup needed.
Source code in src/watlowlib/devices/loop.py
read_alarms
async
¶
Read the alarm word for this loop.
Currently raises :class:watlowlib.errors.WatlowProtocolUnsupportedError —
see :func:watlowlib.commands.alarms.read_alarms for why the
decoder is not yet wired up.
Source code in src/watlowlib/devices/loop.py
read_output
async
¶
read_pid
async
¶
Read every PID gain for this loop. Missing gains return None.
Cool-side gains (cool_proportional_band, dead_band)
are skipped when the controller's identified capabilities
lack :attr:Capability.HAS_COOLING (e.g. PM output_2 ==
'A'). Pre-identify, the gate is permissive.
Source code in src/watlowlib/devices/loop.py
read_pv
async
¶
read_setpoint
async
¶
set_setpoint
async
¶
Write this loop's setpoint (RWES → confirm=True required).
Source code in src/watlowlib/devices/loop.py
write_pid
async
¶
Write the supplied gains for this loop.
Persistent — passing confirm=True is required. Fields
left None on gains skip the wire entirely. Setting a
cool-side field on a controller without
:attr:Capability.HAS_COOLING raises
:class:watlowlib.errors.WatlowConfigurationError.
Source code in src/watlowlib/devices/loop.py
Capability + safety + availability¶
watlowlib.devices.capability ¶
Three small enums that set the contract between layers.
- :class:
SafetyTier— derived from RWES; gatesconfirm=Truewrites. - :class:
Capability— coarse hardware-feature bitmap. Bits are added when a captured family needs them and existing values stay stable. - :class:
Availability— per-command session cache state.
This module is leaf — it imports nothing from
:mod:watlowlib.devices siblings, so the registry and command layers
can pull these enums without an import cycle. See docs/design.md
§5b.
Availability ¶
Bases: StrEnum
Per-command session state.
Sticky for the session: once a command transitions to
:attr:UNSUPPORTED, the session short-circuits subsequent
invocations with a typed error pre-I/O. The transition table
lives in docs/design.md §5b.
Capability ¶
Bases: Flag
Coarse hardware capability bits.
Bits are derived from a decoded part number when one is available
(see :func:watlowlib.registry.families.capabilities_for_part_number)
and fall back to a per-family prior otherwise. The session widens
the set at runtime when a command succeeds against a parameter
that proves the capability.
The vocabulary is small on purpose — most Watlow gating is by
:class:watlowlib.registry.families.ControllerFamily and by
:attr:watlowlib.registry.parameters.ParameterSpec.parameter_id,
not by per-feature bits. New bits are added when captured family
behaviour requires them.
SafetyTier ¶
Bases: IntEnum
How dangerous a command is to invoke.
READ_ONLY(R) — no state change.STATEFUL— runtime state change but not EEPROM-backed. Reserved for commands like "start autotune"; no PM parameter maps here today, but the tier exists so future commands have a place to live.PERSISTENT(RW / RWE / RWES) — EEPROM-backed; requiresconfirm=Trueat the facade.
capabilities_for_family ¶
Return the capability prior for family.
The session promotes observed capabilities at runtime and the part-
number decoder fills in per-SKU bits via
:func:watlowlib.registry.families.capabilities_for_part_number.
PM is intentionally :attr:Capability.NONE because PM SKUs vary
across every dimension (cooling / modbus / profile / comms).
Source code in src/watlowlib/devices/capability.py
Family classification¶
watlowlib.devices.kind ¶
Re-export :class:ControllerFamily under :mod:watlowlib.devices.
Callers can import it from either location. The canonical home is
:mod:watlowlib.registry.families so the registry layer can construct
family enums without depending on :mod:watlowlib.devices.
ControllerFamily ¶
Bases: StrEnum
Watlow controller family discriminator.
Membership here is advisory — :class:watlowlib.devices.session.Session
treats family hints as priors, not gates. See docs/design.md §5b.
classify_family ¶
Return the :class:ControllerFamily for a part-number string.
Only the leading family discriminator is parsed; per-family digit
decoding is in :func:decode_part_number.
Source code in src/watlowlib/registry/families.py
Public dataclasses¶
watlowlib.devices.models ¶
Public dataclasses returned by the :class:Controller facade.
All frozen, slots=True. py.typed ships.
See docs/design.md §6a.
AlarmState
dataclass
¶
Decoded alarm bits for one loop.
DeviceHealth ¶
Bases: StrEnum
Outcome of an :meth:Controller.identify call.
Used by callers (the maintenance verify pass, the configure CLI, discovery rows) to distinguish "the device answered every probe" from "the device answered some probes but not the load-bearing part-number read." Sentinel values stay the same enum across the public API so downstream code can branch on it.
DeviceInfo
dataclass
¶
DeviceInfo(
part_number,
hardware_id,
firmware_id,
serial_number,
family,
protocol,
address,
capabilities,
serial_settings,
loops,
health=DeviceHealth.OK,
configured_protocol=None,
)
Identity + connection metadata for an open controller.
Returned by :meth:Controller.identify. Capabilities are decoded
from the part number when one is captured (see
:func:watlowlib.registry.families.capabilities_for_part_number)
and OR-ed with the family prior; unobserved bits stay zero rather
than being guessed.
protocol is the wire protocol the host is currently talking;
configured_protocol is what the device's persistent EEPROM
parameter (PM 17009) reports. They normally match, but when they
diverge the helper :attr:protocol_mismatch flags it — useful
for catching SKU/firmware combinations where the user wrote a new
protocol but the runtime stack didn't pick it up (e.g. comms
position-8 = 'A', no Modbus stack present even though 17009 reads
1057).
protocol_mismatch
property
¶
True when EEPROM says one protocol and we're talking another.
Always False when :attr:configured_protocol is None
(i.e. identify did not query parameter 17009).
DiscoveryResult
dataclass
¶
One probe attempt's outcome from :func:find_devices.
Cross-library shape (mirrors :mod:alicatlib, :mod:sartoriuslib,
:mod:nidaqlib) so GUI Discover dialogs and capa-style
adapters can filter responsive vs silent rows on a single
attribute and consume the same field set across vendors.
A populated :attr:device_info carries the full
:class:Controller.identify result, including
:attr:DeviceInfo.health and (when the scan queried it)
:attr:DeviceInfo.configured_protocol.
The address field is typed str | int | None to match the
cross-library spec; in practice every watlow probe carries an
int.
LoopState
dataclass
¶
Snapshot of one loop. Composed from several reads.
ParameterEntry
dataclass
¶
Generic registry-driven read/write result.
Returned by :data:watlowlib.commands.READ_PARAMETER and
:data:watlowlib.commands.WRITE_PARAMETER. The
:class:Controller translates an entry into a :class:Reading /
:class:PartNumber / etc. when the public API guarantees a
richer shape.
PartNumber
dataclass
¶
Parsed part-number string returned by read_part_number.
Per-family digit decoding is contributed by
:mod:watlowlib.registry.families. Decoded fragments live in
:attr:details as a free-form mapping so each family can populate
only what its ordering format defines, and so adding fragments to
the PM decoder later is non-breaking.
The EZ-ZONE PM decoder populates case size, control type, power
input, three output codes, and options string. Other families fall
through to a stub: only :attr:family is set, and :attr:details
is empty.
Reading
dataclass
¶
A single timestamped value from the controller.
protocol is set by the variant decoder, not by the facade —
it reflects which wire protocol produced the value (per
docs/design.md invariant 7).
Factory¶
watlowlib.devices.factory ¶
open_device — single entry point for opening a controller.
Honours :attr:ProtocolKind.STDBUS, :attr:ProtocolKind.MODBUS_RTU,
and :attr:ProtocolKind.AUTO (Std Bus probe → Modbus probe → fail).
The detector itself lives in :mod:watlowlib.protocol.detect; the
factory only orchestrates.
The factory does not sweep bauds — the user sets one. See
docs/design.md §7 for why baud sweeping is opt-in via the
watlow-discover CLI rather than the open path.
coerce_wire_temperature_unit ¶
Normalise the assert_wire_temperature_unit kwarg.
Accepts a :class:Unit, a case-insensitive alias, or None.
Rejects :attr:Unit.PERCENT pre-I/O — a temperature scale must
be °C or °F.
Source code in src/watlowlib/devices/factory.py
open_device
async
¶
open_device(
port,
*,
profile=EZZONE_PROFILE,
protocol=None,
address=1,
serial_settings=None,
assert_wire_temperature_unit=None,
identify=True,
)
Open a controller on a serial port.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
port
|
str
|
Serial-port path ( |
required |
profile
|
DeviceProfile
|
The device profile to open against. Defaults to
:data: |
EZZONE_PROFILE
|
protocol
|
ProtocolKind | None
|
Wire protocol. |
None
|
address
|
int
|
Bus address. Std Bus accepts |
1
|
serial_settings
|
SerialSettings | None
|
Optional framing override. |
None
|
identify
|
bool
|
When |
True
|
assert_wire_temperature_unit
|
Unit | str | None
|
User-asserted scale of
temperature values on the wire. Sets
:class: |
None
|
Returns:
| Type | Description |
|---|---|
Controller
|
An opened :class: |
Controller
|
meth: |
Controller
|
Every protocol ( |
Controller
|
an opened controller; |
Controller
|
|
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
WatlowValidationError
|
|
WatlowProtocolUnsupportedError
|
|
Source code in src/watlowlib/devices/factory.py
34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | |
Discovery¶
watlowlib.devices.discovery ¶
Port-scan discovery for watlow-discover and capa-style adapters.
:func:find_devices is the single discovery entry point. It walks the
cartesian product of ports × baudrates × protocols × addresses and
returns one :class:DiscoveryResult per probe attempt — same shape
the sibling libraries (alicat, sartorius, nidaq) emit, so a GUI
Discover dialog can filter on a single ok flag.
The scan is read-only: every probe is a bounded
:meth:Controller.identify call. No setpoint writes, no parameter
writes, no comms-unit-label probes — discovery routinely runs on rigs
that already have other software talking to the same controller.
Per-(port, baudrate, protocol) combination, the transport is opened
once and every address is probed against the same handle. Standard
Bus addresses live in the BACnet MS/TP outer-frame dst MAC byte;
the Modbus bus driver multiplexes slaves over a single open serial
handle. Reopening per address would add ~0.5 s of cdc_acm re-init per
probe on Linux for no benefit.
If a port fails to open at all, it is marked dead for the rest of the
scan: every subsequent (baudrate × protocol) combination for that
port short-circuits with a single :class:DiscoveryResult per planned
address carrying the open error. This avoids hammering a port that
anyserial listed but the kernel won't let us touch.
The default scan is narrow on purpose:
addressesdefaults to(1,)— Modbus RTU allows 1..247, but a multi-port × multi-baud × multi-protocol scan with a 16-address default explodes the probe count and the wall-clock for a GUI scan past what operators tolerate. Callers that need a full address sweep passaddresses=range(1, 248)explicitly.baudratesdefaults to(38400, 19200, 9600)— the EZ-ZONE PM ships at 38400 on Std Bus and 9600 on Modbus RTU; 19200 covers rigs that have been re-configured to the middle baud.protocolsdefaults to(STDBUS, MODBUS_RTU)— both Watlow wire protocols on serial.AUTOis not in the default set; it would double the open count per (port, baudrate) and the detector is intentionally a single-port API.
find_devices
async
¶
find_devices(
*,
ports=None,
addresses=None,
baudrates=None,
protocols=None,
profiles=None,
serial_template=None,
per_probe_timeout_s=_DEFAULT_PROBE_TIMEOUT_S,
)
Probe local serial ports for Watlow controllers.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ports
|
Sequence[str] | None
|
Serial-port paths to scan. |
None
|
addresses
|
Sequence[int] | None
|
Bus addresses to probe per (port, baudrate, protocol)
combination. Defaults to :data: |
None
|
baudrates
|
Sequence[int] | None
|
Baud rates to try. Defaults to
:data: |
None
|
protocols
|
Sequence[ProtocolKind] | None
|
Wire protocols to probe. Defaults to
:data: |
None
|
profiles
|
Sequence[DeviceProfile] | None
|
Device profiles to probe. When given, discovery
iterates profiles instead of |
None
|
serial_template
|
SerialSettings | None
|
Optional :class: |
None
|
per_probe_timeout_s
|
float
|
Per-probe budget. Bounds the
:meth: |
_DEFAULT_PROBE_TIMEOUT_S
|
Returns:
| Name | Type | Description |
|---|---|---|
One |
list[DiscoveryResult]
|
class: |
list[DiscoveryResult]
|
address) tuple, in input order. The cartesian product is |
|
list[DiscoveryResult]
|
iterated outermost-port, then baudrate, then protocol, then |
|
list[DiscoveryResult]
|
address — same input → same output ordering. |
Raises:
| Type | Description |
|---|---|
WatlowConfigurationError
|
|
Notes
- Read-only. Discovery never writes to the device; it
only calls :meth:
Controller.identify(four parameter reads). Safe to run on rigs that already have other software talking to the controller. - Per-port short-circuit. If a port fails to open with a
:class:
WatlowConnectionError, the rest of the scan for that port emitsok=Falserows without re-attempting the open. This avoids hammering a port the kernel won't give us.
Source code in src/watlowlib/devices/discovery.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 | |