Multi-protocol industrial communications library with a four-layer architecture (Client / Driver / Unit / Device) for VFDs, I/O modules, sensors, and motor controllers.
Project description
procaaso-field-device
Industrial-automation communication library for VFDs, motor controllers, I/O modules, and sensors. The active codebase is a layered rework whose design philosophy is documented below — read it before adding a new device, a new protocol, or a new unit archetype.
Note. This library was previously published on PyPI as
procaaso-eip. It has been renamed toprocaaso-field-deviceto reflect its scope as a general multi-protocol hardware-interface library rather than an EtherNet/IP–only package. See CHANGELOG.md for the rename history.
Installation
pip install procaaso-field-device
Requires Python 3.11+.
Design philosophy — the four layers
Every piece of code in this repo belongs to exactly one of four layers. Each layer has a single responsibility and a single direction of dependency: upper layers depend on lower layers; lower layers know nothing about upper layers. Crossing that rule is the most common way to make this library hard to maintain.
┌──────────────────────────────┐
│ Device (planned) │ Scan-loop orchestration
│ composes many Units │ across many Units
└──────────────┬───────────────┘
│ commands
┌──────────────┴───────────────┐
│ Unit (archetype) │ MotorUnit, IOUnit, SensorUnit, ...
│ stable abstract command │ Per-archetype command vocabulary
│ vocabulary + dispatch │ shared by every vendor's driver
└──────────────┬───────────────┘
│ getattr(driver, mapped_name)
┌──────────────┴───────────────┐
│ Driver (hardware) │ PowerFlex753Driver, NanotecDriver, ...
│ vendor wire format │ One class per physical device family
│ + unit method_map manifest │
└──────────────┬───────────────┘
│ uses
┌──────────────┴───────────────┐
│ Client (protocol) │ EipSession, ModbusSession, ...
│ device-agnostic transport │ One instance can serve many drivers
└──────────────────────────────┘
1. Client layer — the protocol
A Client is device-agnostic. It implements a communication protocol (EtherNet/IP, Modbus TCP, CANopen, etc.) and exposes a session object that drivers can borrow. A Client MUST NOT contain any logic specific to a particular vendor's device. If a piece of code only makes sense when talking to a PowerFlex, it does not belong in the Client.
A Client is always its own object with its own lifecycle (connect,
disconnect, request/reply primitives). The rest of the library is
designed around the idea that one Client instance can be passed into
many Drivers — multiple devices sharing one TCP session on a chassis,
for example. The Client's only contract is "can the Driver hand me a
request and get a reply back."
Today's reference implementations:
- EtherNet/IP —
EipSessionatsrc/procaaso_field_device/clients/ethernet_ip/. Covers the EtherNet/IP encapsulation header, the Common Packet Format body, CIP messaging, Forward_Open / Forward_Close, and the explicit request/reply round-trip. - Modbus TCP —
ModbusTcpSessionatsrc/procaaso_field_device/clients/modbus_tcp/. Covers the MBAP header, function-code framing, and the request/reply round-trip.
Protocol-level facts for each client live in a PROTOCOL_NOTES.md
next to the implementation (see
clients/ethernet_ip/PROTOCOL_NOTES.md and
clients/modbus_tcp/PROTOCOL_NOTES.md).
Each Client lives under src/procaaso_field_device/clients/<protocol>/ and is
never coupled to a specific Driver. The Client and the Drivers
that use it are in sibling folders, not nested — the Driver borrows
the Client through dependency injection, never the other way around.
2. Driver layer — the hardware
A Driver is hardware-specific. One Driver class per physical device family (PowerFlex 753, Nanotec C5-E, Moxa E1212, etc.). It encodes:
- Vendor-specific wire-format details (CIP class/instance/attribute numbers, parameter-instance math, bit-position tables, encoding asymmetries).
- The set of read/write methods that exercise those details
(
read_logic_status_word,write_speed_reference,read_motor_poles, ...). - A list of compatible Clients — today enforced by a typed
parameter on
__init__(if not isinstance(session, EipSession): raise DriverError(...)); as more protocols come online this will generalize to an explicit allowlist. - Unit method-map manifests — one class attribute per unit archetype the driver can serve (see §3 and the dedicated section below).
A Driver takes its Client through the constructor; it never reaches out to construct one. That keeps the Client/Driver coupling one-way and preserves the "one Client serves many Drivers" property.
Drivers live under src/procaaso_field_device/drivers/<category>/<vendor>/<model>/.
The category mirrors the unit archetype the driver serves (vsd/ for
variable-speed drives, io/ for I/O modules; future sensor/ /
position/ folders for those archetypes).
Reference implementations today:
- PowerFlex 750-series VFDs —
drivers/vsd/powerflex/{base,pf753,pf755}/. Thebase/package holds shared 750-series logic and the motor-unit method-map manifest (base/unit_config.py);pf753/andpf755/carry the model-specific config, constants, and driver classes. - PowerFlex Series 22 I/O option module —
drivers/vsd/powerflex/series_22_io_option_module/. Add-on I/O for the 750-series chassis; serves the IO unit archetype with its own method-map. - Moxa E1200 I/O modules —
drivers/io/moxa/e1200/. Family-style driver covering the E1210, E1211, E1212, E1213, E1214, E1240, E1241, E1242, E1260, and E1262.
Every non-trivial decision in a driver points to an anchor in a
sibling DRIVER_NOTES.md (one per model package), so an AI agent
walking the code can read the justification without inflating the
source. The PF755 integration story (where it diverges from PF753) is
captured in drivers/vsd/powerflex/PF755_INTEGRATION.md.
3. Unit layer — the archetype
A Unit is archetype-agnostic within a category. Every Driver that
controls a motor is interacted with through the same abstract
command vocabulary — command_set_setpoint, command_start,
command_stop, command_forward, command_reverse,
command_clear_fault. Every Driver that exposes I/O channels will
eventually go through an IOUnit with its own vocabulary, and so on
for SensorUnit, PositionUnit, and future archetypes.
The Unit is the layer where modularity becomes real:
- A scan loop, an HMI, or a control block calls
unit.command_X(...)and gets the same behavior regardless of which vendor sits underneath. Swapping a PowerFlex for a Yaskawa or a Nanotec is a driver-side concern; the Unit-level call sites do not change. - The Unit forwards the call to the Driver via a method-map that the Driver publishes as a class attribute. The Unit has no knowledge of vendor method names. See the next section for the full contract.
- Driver methods that aren't in the abstract vocabulary still bubble
up through
unit.drv. Anything PowerFlex-specific (fault-queue introspection, IO option-card writes, nameplate parameter reads, Forward_Open lifecycle) is still callable asunit.drv.<method>(). The Unit narrows the common surface without hiding the rest.
Naming: the existing implementations are MotorUnit and IOUnit, but
the pattern is not archetype-specific. Future units (SensorUnit,
PositionUnit, ...) follow the same construction: a per-archetype
command vocabulary, a driver-side method-map under a conventional
attribute name (<archetype>_unit_method_map), and the same
.drv escape hatch. When you add a new archetype, the design pattern
already exists — copy MotorUnit's or IOUnit's structure, change
the vocabulary, and ship. See
docs/unit/adding-a-new-unit-archetype.md for the step-by-step.
Unit-level contracts are documented end-to-end in
src/procaaso_field_device/units/motor/UNIT_NOTES.md and
src/procaaso_field_device/units/io/UNIT_NOTES.md. The PowerFlex
750-series driver-side manifest is at
src/procaaso_field_device/drivers/vsd/powerflex/base/unit_config.py.
4. Device layer — orchestration (planned)
A Device composes multiple Units into a coherent piece of plant equipment with its own execution loop — a pump skid with a motor unit, an IO unit reading flow / pressure, and a sensor unit reading temperature, all stepping in lock-step on a single scan cycle.
This layer is planned but not yet implemented. The folder is
checked in as src/procaaso_field_device/device/ with a README describing
its anticipated structure. New Driver and Unit
work should be designed with this layer in mind — most importantly,
keeping per-call latency under the 5 ms scan budget the rework targets.
The driver↔unit method-map contract
This is the central mechanism that makes the Unit layer work. If you are adding a new driver, this is the contract you MUST satisfy.
What the driver publishes
For every Unit archetype the Driver can serve, the Driver declares a
class attribute named <archetype>_unit_method_map whose value is a
dict[str, str] mapping abstract command names to concrete driver
method names:
class PowerFlex753Driver:
motor_unit_method_map = {
# Required on every motor driver.
"command_set_setpoint": "write_speed_reference",
"command_start": "write_command_start",
"command_stop": "write_command_stop",
# Optional — PowerFlex supports these, so they're mapped.
"command_forward": "write_command_forward",
"command_reverse": "write_command_reverse",
"command_clear_fault": "write_command_clear_fault",
}
The attribute name is a convention, not an import. The Driver does
NOT import anything from the Unit package. The Unit knows to look for
motor_unit_method_map (or io_unit_method_map, etc.) on whatever
driver it is handed. This is what keeps the dependency direction
one-way: Units know about Drivers; Drivers do not know about Units.
A single driver class can publish multiple method-map attributes — one
per unit archetype it can serve. A combined VFD+IO driver, for example,
would declare both a motor_unit_method_map and an
io_unit_method_map.
What the unit does at construction
MotorUnit.__init__ (and every future archetype unit's __init__)
performs a boot-time validation pass:
- The driver has the expected method-map attribute.
- The attribute is a
dict. - Every entry in
REQUIRED_COMMANDSfor that archetype is present in the map. - Every mapped method name actually resolves to a callable on the driver.
A misconfigured pairing fails immediately at construction with a
ConfigurationError, never silently at first call. If
MotorUnit(config, driver=X) returns, driver X is contract-compliant
at the structural level for that archetype.
What the unit does at runtime
Every unit.command_X(...) call walks through _dispatch, which:
- Looks up
command_Xin the driver's method-map. - If absent (an optional command the driver didn't map), raises
UnsupportedCommandErrorwith the list of supported commands. - If present, calls
getattr(self._driver, mapped_name)(*args, **kwargs)and returns whatever the driver method returned.
No allocation. No serialization. No transformation of arguments. The Unit is a thin dispatcher; the wire-level work lives in the driver method.
The escape hatch — unit.drv
Every Unit exposes unit.drv, which returns the bound driver. Use it
for any driver method that the abstract vocabulary deliberately doesn't
cover:
unit = MotorUnit(config, driver=powerflex)
unit.command_start() # abstract, vendor-neutral
unit.command_set_setpoint(50.0) # abstract, vendor-neutral
# Vendor-specific — bubble up through the escape hatch:
faults = unit.drv.read_fault_queue_count()
record = unit.drv.read_fault(1)
unit.drv.write_io_analog_output_value(channel=0, value=4.0)
This keeps the abstract surface intentionally small (only the things
that generalize across every vendor of that archetype), while still
giving callers full access to the driver's specialised surface when
they need it. A unit.drv.X() call site is a deliberate, visible
admission that the caller is reaching past the abstraction; it is
not a workaround for an incomplete map.
Recipe: onboarding a new driver
- Implement the driver class in
src/procaaso_field_device/drivers/<protocol>/<vendor>/driver.py. Whatever method names fit the vendor's manuals are fine; the Unit doesn't care. - Declare the method-map(s) as plain
dict[str, str]constants in a siblingunit_config.pynext to the driver. - In the driver class body, alias each constant to its conventional
attribute name (
motor_unit_method_map = MOTOR_UNIT_METHOD_MAP). - Optionally write a
DRIVER_NOTES.mdnext to the driver capturing the vendor-specific quirks worth remembering. - Construct the matching Unit against the driver:
MotorUnit(MotorUnitConfig(...), driver=YourDriver(...)). If construction succeeds, the contract is satisfied.
The unit-side code does not change. Every existing call site for that archetype continues to work.
Repository orientation
| Path | Status | What lives there |
|---|---|---|
src/procaaso_field_device/clients/<protocol>/ |
Client (Layer 1) | One folder per communication protocol. Today: ethernet_ip/, modbus_tcp/. |
src/procaaso_field_device/drivers/<category>/<vendor>/<model>/ |
Driver (Layer 2) | One folder per device model, grouped by vendor and category. Today: vsd/powerflex/{base,pf753,pf755,series_22_io_option_module}/, io/moxa/e1200/. |
src/procaaso_field_device/units/<archetype>/ |
Unit (Layer 3) | One folder per unit archetype. Today: motor/, io/. |
src/procaaso_field_device/device/ |
Device (Layer 4) | Planned orchestration layer. Empty placeholder with a README; not implemented yet. |
src/procaaso_field_device/common/ |
Shared | SignalValue, StatusCode, scaling, filtering, safety, exceptions, diagnostics, modes. Used by every layer. |
docs/ |
Design | library-modules-reference.md (design guide), notes-index.md (anchor catalog), setup-and-conventions.md, and per-layer READMEs under client/, driver/, unit/, device/. |
examples/ |
Demos | Bench-shaped pytest scripts exercising the library against real hardware (PF753, PF755, Series 22 IO, Moxa E1200). |
tests/ |
Tests | pytest mirror of src/procaaso_field_device/ plus driver-contract conformance suites under tests/contracts/. |
tools/ |
Tools | Static-analysis helpers (safety_analyzer.py, check_slots.py) and gen_notes_index.py. |
manuals/ |
Reference | Vendor PDFs and Markdown extractions cited by NOTES files. |
legacy/ |
Archive | Reserved for any pre-rework code preserved for reference. Currently empty. |
Where to read more
docs/library-modules-reference.md— design guide; the source for the architectural decisions in this README.docs/setup-and-conventions.md— repo-level setup, layered architecture summary, and status-propagation contract.docs/unit/extending-unit-commands.md— procedural guide for adding new REQUIRED or OPTIONAL commands to any unit archetype; documents the full blast radius (code + docs + tests).docs/unit/adding-a-new-unit-archetype.md— step-by-step for introducing a brand-new unit archetype alongsidemotor/andio/.docs/notes-index.md— generated grep-friendly catalog of every anchor across the NOTES files. Regenerate withpython3 tools/gen_notes_index.py > docs/notes-index.md.src/procaaso_field_device/clients/ethernet_ip/PROTOCOL_NOTES.md— wire-format conventions and protocol-level quirks for EtherNet/IP.src/procaaso_field_device/clients/modbus_tcp/PROTOCOL_NOTES.md— wire-format conventions for Modbus TCP.src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md— PowerFlex 750-series shared driver notes.src/procaaso_field_device/drivers/vsd/powerflex/pf753/DRIVER_NOTES.mdand.../pf755/DRIVER_NOTES.md— model-specific quirks.src/procaaso_field_device/drivers/vsd/powerflex/PF755_INTEGRATION.md— PF755 vs PF753 integration delta.src/procaaso_field_device/drivers/io/moxa/e1200/DRIVER_NOTES.md— Moxa E1200 family driver notes.src/procaaso_field_device/units/motor/UNIT_NOTES.md— motor-unit driver contract in full.src/procaaso_field_device/units/io/UNIT_NOTES.md— IO-unit driver contract in full.src/procaaso_field_device/device/README.md— placeholder describing the planned Device layer (Layer 4) and its anticipated structure.
Supported hardware
| Device | Unit archetype | Client | Driver location |
|---|---|---|---|
| Allen-Bradley PowerFlex 753 | MotorUnit |
EtherNet/IP (EipSession) |
drivers/vsd/powerflex/pf753/ |
| Allen-Bradley PowerFlex 755 | MotorUnit |
EtherNet/IP (EipSession) |
drivers/vsd/powerflex/pf755/ |
| Allen-Bradley Series 22 I/O option module | IOUnit |
EtherNet/IP (EipSession) |
drivers/vsd/powerflex/series_22_io_option_module/ |
| Moxa ioLogik E1210 / E1211 / E1212 / E1213 / E1214 | IOUnit |
Modbus TCP (ModbusTcpSession) |
drivers/io/moxa/e1200/ |
| Moxa ioLogik E1240 / E1241 / E1242 | IOUnit |
Modbus TCP (ModbusTcpSession) |
drivers/io/moxa/e1200/ |
| Moxa ioLogik E1260 / E1262 | IOUnit |
Modbus TCP (ModbusTcpSession) |
drivers/io/moxa/e1200/ |
Quick example
from procaaso_field_device.clients.ethernet_ip.client import EipSession, EipSessionConfig
from procaaso_field_device.drivers.vsd.powerflex.pf753.config import PowerFlex753Config
from procaaso_field_device.drivers.vsd.powerflex.pf753.driver import PowerFlex753Driver
from procaaso_field_device.units.motor import MotorUnit, MotorUnitConfig
# Client — protocol only, no device knowledge.
session = EipSession(EipSessionConfig(host="192.168.1.10"))
session.connect()
# Driver — borrows the Client, encodes vendor-specific wire format.
drive = PowerFlex753Driver(session, PowerFlex753Config())
# Unit — abstract motor vocabulary, vendor-neutral call sites.
motor = MotorUnit(MotorUnitConfig(), driver=drive)
motor.command_start()
motor.command_set_setpoint(50.0) # Hz, abstract
# ...
motor.command_stop()
# Escape hatch for driver-specific reads:
faults = motor.drv.read_fault_queue_count()
session.disconnect()
For a runnable bench-test version of this flow, see
examples/motor_unit_test_pf753.py.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file procaaso_field_device-0.0.1.tar.gz.
File metadata
- Download URL: procaaso_field_device-0.0.1.tar.gz
- Upload date:
- Size: 235.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.12.10 Windows/11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5e7510735c3f370955e560a2c05d1d77e0303e12135d5c901a8ec9703cb85239
|
|
| MD5 |
c8ea861e7164d438fd04bc1a8bd7b443
|
|
| BLAKE2b-256 |
e235a9b49d4afbe47263e2310bb58c2b14c83b3d4f8c84f2bc130590bf0caf91
|
File details
Details for the file procaaso_field_device-0.0.1-py3-none-any.whl.
File metadata
- Download URL: procaaso_field_device-0.0.1-py3-none-any.whl
- Upload date:
- Size: 286.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.3 CPython/3.12.10 Windows/11
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5f210071248b2add6b2d646959de68e99486e4ee13951a707edb0361cd6f8311
|
|
| MD5 |
c98d5d9f05c050d0e1ec1b8505c771de
|
|
| BLAKE2b-256 |
f5f2f22eb665c9b257618accea7f68469142ab8ab14b56104d40dbad9ffc4460
|