procaaso-field-device 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- procaaso_field_device-0.0.1/LICENSE +21 -0
- procaaso_field_device-0.0.1/PKG-INFO +420 -0
- procaaso_field_device-0.0.1/README.md +396 -0
- procaaso_field_device-0.0.1/pyproject.toml +79 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/__init__.py +7 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/__init__.py +16 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/PROTOCOL_NOTES.md +481 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/__init__.py +15 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/client.py +697 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/codec.py +181 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/messaging.py +417 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/transport.py +709 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/PROTOCOL_NOTES.md +414 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/__init__.py +21 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/client.py +719 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/codec.py +329 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/messaging.py +808 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/transport.py +244 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/__init__.py +6 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/diagnostics.py +66 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/exceptions.py +69 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/filtering.py +100 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/modes.py +91 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/safety.py +120 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/scaling.py +156 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/common/status.py +332 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/device/README.md +80 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/device/__init__.py +0 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/__init__.py +7 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/__init__.py +7 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/__init__.py +6 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/DRIVER_NOTES.md +624 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/__init__.py +60 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/base.py +873 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/config.py +191 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/constants.py +199 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1210.py +25 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1211.py +22 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1212.py +29 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1213.py +23 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1214.py +23 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1240.py +28 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1241.py +24 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1242.py +47 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1260.py +27 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1262.py +27 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/signals.py +122 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/__init__.py +8 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/PF755_INTEGRATION.md +637 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/__init__.py +25 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md +1452 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md.delete-me +5 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/__init__.py +29 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/config.py +62 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/constants.py +594 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/driver.py +2509 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/identity.py +59 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/signals.py +598 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/unit_config.py +28 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/DRIVER_NOTES.md +197 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/__init__.py +15 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/config.py +27 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/constants.py +46 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/driver.py +118 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/DRIVER_NOTES.md +260 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/DRIVER_NOTES.md.tmp +179 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/__init__.py +14 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/config.py +32 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/constants.py +135 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/driver.py +344 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/driver.py.new +1 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/__init__.py +33 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/config.py +65 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/constants.py +304 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/driver.py +679 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/signals.py +61 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/unit_config.py +36 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/__init__.py +26 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/UNIT_NOTES.md +994 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/__init__.py +43 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/config_schema.py +32 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/driver_config.py +158 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/io.py +351 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/UNIT_NOTES.md +786 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/__init__.py +40 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/config_schema.py +30 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/driver_config.py +119 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/driver_protocol.py +131 -0
- procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/motor.py +208 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 ConSynSys-Automation
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: procaaso-field-device
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Multi-protocol industrial communications library with a four-layer architecture (Client / Driver / Unit / Device) for VFDs, I/O modules, sensors, and motor controllers.
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: ethernetip,eip,modbus,modbus-tcp,industrial-automation,cip,vfd,motor-control,powerflex,moxa,iologik,io-modules
|
|
7
|
+
Author: ProCaaSo
|
|
8
|
+
Requires-Python: >=3.11,<4.0
|
|
9
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Manufacturing
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Topic :: System :: Hardware :: Hardware Drivers
|
|
20
|
+
Classifier: Topic :: System :: Networking
|
|
21
|
+
Project-URL: Homepage, https://github.com/ConSynSys-Automation/procaaso-field-device
|
|
22
|
+
Project-URL: Repository, https://github.com/ConSynSys-Automation/procaaso-field-device
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# procaaso-field-device
|
|
26
|
+
|
|
27
|
+
Industrial-automation communication library for VFDs, motor controllers,
|
|
28
|
+
I/O modules, and sensors. The active codebase is a layered rework whose
|
|
29
|
+
design philosophy is documented below — read it before adding a new
|
|
30
|
+
device, a new protocol, or a new unit archetype.
|
|
31
|
+
|
|
32
|
+
> **Note.** This library was previously published on PyPI as
|
|
33
|
+
> `procaaso-eip`. It has been renamed to **`procaaso-field-device`** to
|
|
34
|
+
> reflect its scope as a general multi-protocol hardware-interface
|
|
35
|
+
> library rather than an EtherNet/IP–only package. See
|
|
36
|
+
> [CHANGELOG.md](CHANGELOG.md) for the rename history.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install procaaso-field-device
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires Python 3.11+.
|
|
45
|
+
|
|
46
|
+
## Design philosophy — the four layers
|
|
47
|
+
|
|
48
|
+
Every piece of code in this repo belongs to exactly one of four layers.
|
|
49
|
+
Each layer has a single responsibility and a single direction of
|
|
50
|
+
dependency: **upper layers depend on lower layers; lower layers know
|
|
51
|
+
nothing about upper layers.** Crossing that rule is the most common way
|
|
52
|
+
to make this library hard to maintain.
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
┌──────────────────────────────┐
|
|
56
|
+
│ Device (planned) │ Scan-loop orchestration
|
|
57
|
+
│ composes many Units │ across many Units
|
|
58
|
+
└──────────────┬───────────────┘
|
|
59
|
+
│ commands
|
|
60
|
+
┌──────────────┴───────────────┐
|
|
61
|
+
│ Unit (archetype) │ MotorUnit, IOUnit, SensorUnit, ...
|
|
62
|
+
│ stable abstract command │ Per-archetype command vocabulary
|
|
63
|
+
│ vocabulary + dispatch │ shared by every vendor's driver
|
|
64
|
+
└──────────────┬───────────────┘
|
|
65
|
+
│ getattr(driver, mapped_name)
|
|
66
|
+
┌──────────────┴───────────────┐
|
|
67
|
+
│ Driver (hardware) │ PowerFlex753Driver, NanotecDriver, ...
|
|
68
|
+
│ vendor wire format │ One class per physical device family
|
|
69
|
+
│ + unit method_map manifest │
|
|
70
|
+
└──────────────┬───────────────┘
|
|
71
|
+
│ uses
|
|
72
|
+
┌──────────────┴───────────────┐
|
|
73
|
+
│ Client (protocol) │ EipSession, ModbusSession, ...
|
|
74
|
+
│ device-agnostic transport │ One instance can serve many drivers
|
|
75
|
+
└──────────────────────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 1. Client layer — the protocol
|
|
79
|
+
|
|
80
|
+
A Client is **device-agnostic**. It implements a communication protocol
|
|
81
|
+
(EtherNet/IP, Modbus TCP, CANopen, etc.) and exposes a session object
|
|
82
|
+
that drivers can borrow. A Client MUST NOT contain any logic specific to
|
|
83
|
+
a particular vendor's device. If a piece of code only makes sense when
|
|
84
|
+
talking to a PowerFlex, it does not belong in the Client.
|
|
85
|
+
|
|
86
|
+
A Client is always its own object with its own lifecycle (`connect`,
|
|
87
|
+
`disconnect`, request/reply primitives). The rest of the library is
|
|
88
|
+
designed around the idea that **one Client instance can be passed into
|
|
89
|
+
many Drivers** — multiple devices sharing one TCP session on a chassis,
|
|
90
|
+
for example. The Client's only contract is "can the Driver hand me a
|
|
91
|
+
request and get a reply back."
|
|
92
|
+
|
|
93
|
+
Today's reference implementations:
|
|
94
|
+
|
|
95
|
+
- **EtherNet/IP** — `EipSession` at
|
|
96
|
+
`src/procaaso_field_device/clients/ethernet_ip/`. Covers the
|
|
97
|
+
EtherNet/IP encapsulation header, the Common Packet Format body, CIP
|
|
98
|
+
messaging, Forward_Open / Forward_Close, and the explicit
|
|
99
|
+
request/reply round-trip.
|
|
100
|
+
- **Modbus TCP** — `ModbusTcpSession` at
|
|
101
|
+
`src/procaaso_field_device/clients/modbus_tcp/`. Covers the MBAP
|
|
102
|
+
header, function-code framing, and the request/reply round-trip.
|
|
103
|
+
|
|
104
|
+
Protocol-level facts for each client live in a `PROTOCOL_NOTES.md`
|
|
105
|
+
next to the implementation (see
|
|
106
|
+
`clients/ethernet_ip/PROTOCOL_NOTES.md` and
|
|
107
|
+
`clients/modbus_tcp/PROTOCOL_NOTES.md`).
|
|
108
|
+
|
|
109
|
+
Each Client lives under `src/procaaso_field_device/clients/<protocol>/` and is
|
|
110
|
+
**never coupled to a specific Driver**. The Client and the Drivers
|
|
111
|
+
that use it are in sibling folders, not nested — the Driver borrows
|
|
112
|
+
the Client through dependency injection, never the other way around.
|
|
113
|
+
|
|
114
|
+
### 2. Driver layer — the hardware
|
|
115
|
+
|
|
116
|
+
A Driver is **hardware-specific**. One Driver class per physical device
|
|
117
|
+
family (PowerFlex 753, Nanotec C5-E, Moxa E1212, etc.). It encodes:
|
|
118
|
+
|
|
119
|
+
- Vendor-specific wire-format details (CIP class/instance/attribute
|
|
120
|
+
numbers, parameter-instance math, bit-position tables, encoding
|
|
121
|
+
asymmetries).
|
|
122
|
+
- The set of read/write methods that exercise those details
|
|
123
|
+
(`read_logic_status_word`, `write_speed_reference`,
|
|
124
|
+
`read_motor_poles`, ...).
|
|
125
|
+
- A **list of compatible Clients** — today enforced by a typed
|
|
126
|
+
parameter on `__init__` (`if not isinstance(session, EipSession):
|
|
127
|
+
raise DriverError(...)`); as more protocols come online this will
|
|
128
|
+
generalize to an explicit allowlist.
|
|
129
|
+
- **Unit method-map manifests** — one class attribute per unit
|
|
130
|
+
archetype the driver can serve (see §3 and the dedicated section
|
|
131
|
+
below).
|
|
132
|
+
|
|
133
|
+
A Driver takes its Client through the constructor; it never reaches out
|
|
134
|
+
to construct one. That keeps the Client/Driver coupling one-way and
|
|
135
|
+
preserves the "one Client serves many Drivers" property.
|
|
136
|
+
|
|
137
|
+
Drivers live under `src/procaaso_field_device/drivers/<category>/<vendor>/<model>/`.
|
|
138
|
+
The category mirrors the unit archetype the driver serves (`vsd/` for
|
|
139
|
+
variable-speed drives, `io/` for I/O modules; future `sensor/` /
|
|
140
|
+
`position/` folders for those archetypes).
|
|
141
|
+
|
|
142
|
+
Reference implementations today:
|
|
143
|
+
|
|
144
|
+
- **PowerFlex 750-series VFDs** —
|
|
145
|
+
`drivers/vsd/powerflex/{base,pf753,pf755}/`. The `base/` package
|
|
146
|
+
holds shared 750-series logic and the motor-unit method-map manifest
|
|
147
|
+
(`base/unit_config.py`); `pf753/` and `pf755/` carry the
|
|
148
|
+
model-specific config, constants, and driver classes.
|
|
149
|
+
- **PowerFlex Series 22 I/O option module** —
|
|
150
|
+
`drivers/vsd/powerflex/series_22_io_option_module/`. Add-on I/O for
|
|
151
|
+
the 750-series chassis; serves the IO unit archetype with its own
|
|
152
|
+
method-map.
|
|
153
|
+
- **Moxa E1200 I/O modules** —
|
|
154
|
+
`drivers/io/moxa/e1200/`. Family-style driver covering the E1210,
|
|
155
|
+
E1211, E1212, E1213, E1214, E1240, E1241, E1242, E1260, and E1262.
|
|
156
|
+
|
|
157
|
+
Every non-trivial decision in a driver points to an anchor in a
|
|
158
|
+
sibling `DRIVER_NOTES.md` (one per model package), so an AI agent
|
|
159
|
+
walking the code can read the justification without inflating the
|
|
160
|
+
source. The PF755 integration story (where it diverges from PF753) is
|
|
161
|
+
captured in `drivers/vsd/powerflex/PF755_INTEGRATION.md`.
|
|
162
|
+
|
|
163
|
+
### 3. Unit layer — the archetype
|
|
164
|
+
|
|
165
|
+
A Unit is **archetype-agnostic** within a category. Every Driver that
|
|
166
|
+
controls a motor is interacted with through the **same** abstract
|
|
167
|
+
command vocabulary — `command_set_setpoint`, `command_start`,
|
|
168
|
+
`command_stop`, `command_forward`, `command_reverse`,
|
|
169
|
+
`command_clear_fault`. Every Driver that exposes I/O channels will
|
|
170
|
+
eventually go through an `IOUnit` with its own vocabulary, and so on
|
|
171
|
+
for `SensorUnit`, `PositionUnit`, and future archetypes.
|
|
172
|
+
|
|
173
|
+
The Unit is the layer where modularity becomes real:
|
|
174
|
+
|
|
175
|
+
- **A scan loop, an HMI, or a control block calls `unit.command_X(...)`
|
|
176
|
+
and gets the same behavior regardless of which vendor sits
|
|
177
|
+
underneath.** Swapping a PowerFlex for a Yaskawa or a Nanotec is a
|
|
178
|
+
driver-side concern; the Unit-level call sites do not change.
|
|
179
|
+
- **The Unit forwards the call to the Driver via a method-map** that
|
|
180
|
+
the Driver publishes as a class attribute. The Unit has no knowledge
|
|
181
|
+
of vendor method names. See the next section for the full contract.
|
|
182
|
+
- **Driver methods that aren't in the abstract vocabulary still bubble
|
|
183
|
+
up through `unit.drv`.** Anything PowerFlex-specific (fault-queue
|
|
184
|
+
introspection, IO option-card writes, nameplate parameter reads,
|
|
185
|
+
Forward_Open lifecycle) is still callable as `unit.drv.<method>()`.
|
|
186
|
+
The Unit narrows the *common* surface without hiding the rest.
|
|
187
|
+
|
|
188
|
+
Naming: the existing implementations are `MotorUnit` and `IOUnit`, but
|
|
189
|
+
the pattern is **not archetype-specific**. Future units (`SensorUnit`,
|
|
190
|
+
`PositionUnit`, ...) follow the same construction: a per-archetype
|
|
191
|
+
command vocabulary, a driver-side method-map under a conventional
|
|
192
|
+
attribute name (`<archetype>_unit_method_map`), and the same
|
|
193
|
+
`.drv` escape hatch. When you add a new archetype, the design pattern
|
|
194
|
+
already exists — copy `MotorUnit`'s or `IOUnit`'s structure, change
|
|
195
|
+
the vocabulary, and ship. See
|
|
196
|
+
`docs/unit/adding-a-new-unit-archetype.md` for the step-by-step.
|
|
197
|
+
|
|
198
|
+
Unit-level contracts are documented end-to-end in
|
|
199
|
+
`src/procaaso_field_device/units/motor/UNIT_NOTES.md` and
|
|
200
|
+
`src/procaaso_field_device/units/io/UNIT_NOTES.md`. The PowerFlex
|
|
201
|
+
750-series driver-side manifest is at
|
|
202
|
+
`src/procaaso_field_device/drivers/vsd/powerflex/base/unit_config.py`.
|
|
203
|
+
|
|
204
|
+
### 4. Device layer — orchestration (planned)
|
|
205
|
+
|
|
206
|
+
A Device composes multiple Units into a coherent piece of plant
|
|
207
|
+
equipment with its own execution loop — a pump skid with a motor unit,
|
|
208
|
+
an IO unit reading flow / pressure, and a sensor unit reading
|
|
209
|
+
temperature, all stepping in lock-step on a single scan cycle.
|
|
210
|
+
|
|
211
|
+
This layer is **planned but not yet implemented.** The folder is
|
|
212
|
+
checked in as `src/procaaso_field_device/device/` with a README describing
|
|
213
|
+
its anticipated structure. New Driver and Unit
|
|
214
|
+
work should be designed with this layer in mind — most importantly,
|
|
215
|
+
keeping per-call latency under the 5 ms scan budget the rework targets.
|
|
216
|
+
|
|
217
|
+
## The driver↔unit method-map contract
|
|
218
|
+
|
|
219
|
+
This is the central mechanism that makes the Unit layer work. If you
|
|
220
|
+
are adding a new driver, this is the contract you MUST satisfy.
|
|
221
|
+
|
|
222
|
+
### What the driver publishes
|
|
223
|
+
|
|
224
|
+
For every Unit archetype the Driver can serve, the Driver declares a
|
|
225
|
+
class attribute named `<archetype>_unit_method_map` whose value is a
|
|
226
|
+
`dict[str, str]` mapping abstract command names to concrete driver
|
|
227
|
+
method names:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
class PowerFlex753Driver:
|
|
231
|
+
motor_unit_method_map = {
|
|
232
|
+
# Required on every motor driver.
|
|
233
|
+
"command_set_setpoint": "write_speed_reference",
|
|
234
|
+
"command_start": "write_command_start",
|
|
235
|
+
"command_stop": "write_command_stop",
|
|
236
|
+
# Optional — PowerFlex supports these, so they're mapped.
|
|
237
|
+
"command_forward": "write_command_forward",
|
|
238
|
+
"command_reverse": "write_command_reverse",
|
|
239
|
+
"command_clear_fault": "write_command_clear_fault",
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The attribute name is a **convention**, not an import. The Driver does
|
|
244
|
+
NOT import anything from the Unit package. The Unit knows to look for
|
|
245
|
+
`motor_unit_method_map` (or `io_unit_method_map`, etc.) on whatever
|
|
246
|
+
driver it is handed. This is what keeps the dependency direction
|
|
247
|
+
one-way: Units know about Drivers; Drivers do not know about Units.
|
|
248
|
+
|
|
249
|
+
A single driver class can publish multiple method-map attributes — one
|
|
250
|
+
per unit archetype it can serve. A combined VFD+IO driver, for example,
|
|
251
|
+
would declare both a `motor_unit_method_map` and an
|
|
252
|
+
`io_unit_method_map`.
|
|
253
|
+
|
|
254
|
+
### What the unit does at construction
|
|
255
|
+
|
|
256
|
+
`MotorUnit.__init__` (and every future archetype unit's `__init__`)
|
|
257
|
+
performs a **boot-time** validation pass:
|
|
258
|
+
|
|
259
|
+
1. The driver has the expected method-map attribute.
|
|
260
|
+
2. The attribute is a `dict`.
|
|
261
|
+
3. Every entry in `REQUIRED_COMMANDS` for that archetype is present in
|
|
262
|
+
the map.
|
|
263
|
+
4. Every mapped method name actually resolves to a callable on the
|
|
264
|
+
driver.
|
|
265
|
+
|
|
266
|
+
A misconfigured pairing fails immediately at construction with a
|
|
267
|
+
`ConfigurationError`, never silently at first call. If
|
|
268
|
+
`MotorUnit(config, driver=X)` returns, driver `X` is contract-compliant
|
|
269
|
+
at the structural level for that archetype.
|
|
270
|
+
|
|
271
|
+
### What the unit does at runtime
|
|
272
|
+
|
|
273
|
+
Every `unit.command_X(...)` call walks through `_dispatch`, which:
|
|
274
|
+
|
|
275
|
+
1. Looks up `command_X` in the driver's method-map.
|
|
276
|
+
2. If absent (an *optional* command the driver didn't map), raises
|
|
277
|
+
`UnsupportedCommandError` with the list of supported commands.
|
|
278
|
+
3. If present, calls
|
|
279
|
+
`getattr(self._driver, mapped_name)(*args, **kwargs)` and returns
|
|
280
|
+
whatever the driver method returned.
|
|
281
|
+
|
|
282
|
+
No allocation. No serialization. No transformation of arguments. The
|
|
283
|
+
Unit is a thin dispatcher; the wire-level work lives in the driver
|
|
284
|
+
method.
|
|
285
|
+
|
|
286
|
+
### The escape hatch — `unit.drv`
|
|
287
|
+
|
|
288
|
+
Every Unit exposes `unit.drv`, which returns the bound driver. Use it
|
|
289
|
+
for any driver method that the abstract vocabulary deliberately doesn't
|
|
290
|
+
cover:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
unit = MotorUnit(config, driver=powerflex)
|
|
294
|
+
|
|
295
|
+
unit.command_start() # abstract, vendor-neutral
|
|
296
|
+
unit.command_set_setpoint(50.0) # abstract, vendor-neutral
|
|
297
|
+
|
|
298
|
+
# Vendor-specific — bubble up through the escape hatch:
|
|
299
|
+
faults = unit.drv.read_fault_queue_count()
|
|
300
|
+
record = unit.drv.read_fault(1)
|
|
301
|
+
unit.drv.write_io_analog_output_value(channel=0, value=4.0)
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
This keeps the abstract surface intentionally small (only the things
|
|
305
|
+
that generalize across every vendor of that archetype), while still
|
|
306
|
+
giving callers full access to the driver's specialised surface when
|
|
307
|
+
they need it. A `unit.drv.X()` call site is a deliberate, visible
|
|
308
|
+
admission that the caller is reaching past the abstraction; it is
|
|
309
|
+
**not** a workaround for an incomplete map.
|
|
310
|
+
|
|
311
|
+
### Recipe: onboarding a new driver
|
|
312
|
+
|
|
313
|
+
1. Implement the driver class in
|
|
314
|
+
`src/procaaso_field_device/drivers/<protocol>/<vendor>/driver.py`.
|
|
315
|
+
Whatever method names fit the vendor's manuals are fine; the Unit
|
|
316
|
+
doesn't care.
|
|
317
|
+
2. Declare the method-map(s) as plain `dict[str, str]` constants in a
|
|
318
|
+
sibling `unit_config.py` next to the driver.
|
|
319
|
+
3. In the driver class body, alias each constant to its conventional
|
|
320
|
+
attribute name (`motor_unit_method_map = MOTOR_UNIT_METHOD_MAP`).
|
|
321
|
+
4. Optionally write a `DRIVER_NOTES.md` next to the driver capturing
|
|
322
|
+
the vendor-specific quirks worth remembering.
|
|
323
|
+
5. Construct the matching Unit against the driver:
|
|
324
|
+
`MotorUnit(MotorUnitConfig(...), driver=YourDriver(...))`. If
|
|
325
|
+
construction succeeds, the contract is satisfied.
|
|
326
|
+
|
|
327
|
+
The unit-side code does not change. Every existing call site for that
|
|
328
|
+
archetype continues to work.
|
|
329
|
+
|
|
330
|
+
## Repository orientation
|
|
331
|
+
|
|
332
|
+
| Path | Status | What lives there |
|
|
333
|
+
| ----------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------------------------- |
|
|
334
|
+
| `src/procaaso_field_device/clients/<protocol>/` | Client (Layer 1) | One folder per communication protocol. Today: `ethernet_ip/`, `modbus_tcp/`. |
|
|
335
|
+
| `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/`. |
|
|
336
|
+
| `src/procaaso_field_device/units/<archetype>/` | Unit (Layer 3) | One folder per unit archetype. Today: `motor/`, `io/`. |
|
|
337
|
+
| `src/procaaso_field_device/device/` | Device (Layer 4) | Planned orchestration layer. Empty placeholder with a README; not implemented yet. |
|
|
338
|
+
| `src/procaaso_field_device/common/` | Shared | `SignalValue`, `StatusCode`, scaling, filtering, safety, exceptions, diagnostics, modes. Used by every layer. |
|
|
339
|
+
| `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/`. |
|
|
340
|
+
| `examples/` | Demos | Bench-shaped pytest scripts exercising the library against real hardware (PF753, PF755, Series 22 IO, Moxa E1200). |
|
|
341
|
+
| `tests/` | Tests | `pytest` mirror of `src/procaaso_field_device/` plus driver-contract conformance suites under `tests/contracts/`. |
|
|
342
|
+
| `tools/` | Tools | Static-analysis helpers (`safety_analyzer.py`, `check_slots.py`) and `gen_notes_index.py`.|
|
|
343
|
+
| `manuals/` | Reference | Vendor PDFs and Markdown extractions cited by NOTES files. |
|
|
344
|
+
| `legacy/` | Archive | Reserved for any pre-rework code preserved for reference. Currently empty. |
|
|
345
|
+
|
|
346
|
+
### Where to read more
|
|
347
|
+
|
|
348
|
+
- `docs/library-modules-reference.md` — design guide; the source for
|
|
349
|
+
the architectural decisions in this README.
|
|
350
|
+
- `docs/setup-and-conventions.md` — repo-level setup, layered
|
|
351
|
+
architecture summary, and status-propagation contract.
|
|
352
|
+
- `docs/unit/extending-unit-commands.md` — procedural guide for adding
|
|
353
|
+
new REQUIRED or OPTIONAL commands to any unit archetype; documents
|
|
354
|
+
the full blast radius (code + docs + tests).
|
|
355
|
+
- `docs/unit/adding-a-new-unit-archetype.md` — step-by-step for
|
|
356
|
+
introducing a brand-new unit archetype alongside `motor/` and `io/`.
|
|
357
|
+
- `docs/notes-index.md` — generated grep-friendly catalog of every
|
|
358
|
+
anchor across the NOTES files. Regenerate with
|
|
359
|
+
`python3 tools/gen_notes_index.py > docs/notes-index.md`.
|
|
360
|
+
- `src/procaaso_field_device/clients/ethernet_ip/PROTOCOL_NOTES.md` —
|
|
361
|
+
wire-format conventions and protocol-level quirks for EtherNet/IP.
|
|
362
|
+
- `src/procaaso_field_device/clients/modbus_tcp/PROTOCOL_NOTES.md` —
|
|
363
|
+
wire-format conventions for Modbus TCP.
|
|
364
|
+
- `src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md`
|
|
365
|
+
— PowerFlex 750-series shared driver notes.
|
|
366
|
+
- `src/procaaso_field_device/drivers/vsd/powerflex/pf753/DRIVER_NOTES.md`
|
|
367
|
+
and `.../pf755/DRIVER_NOTES.md` — model-specific quirks.
|
|
368
|
+
- `src/procaaso_field_device/drivers/vsd/powerflex/PF755_INTEGRATION.md`
|
|
369
|
+
— PF755 vs PF753 integration delta.
|
|
370
|
+
- `src/procaaso_field_device/drivers/io/moxa/e1200/DRIVER_NOTES.md` —
|
|
371
|
+
Moxa E1200 family driver notes.
|
|
372
|
+
- `src/procaaso_field_device/units/motor/UNIT_NOTES.md` — motor-unit
|
|
373
|
+
driver contract in full.
|
|
374
|
+
- `src/procaaso_field_device/units/io/UNIT_NOTES.md` — IO-unit driver
|
|
375
|
+
contract in full.
|
|
376
|
+
- `src/procaaso_field_device/device/README.md` — placeholder describing
|
|
377
|
+
the planned Device layer (Layer 4) and its anticipated structure.
|
|
378
|
+
|
|
379
|
+
## Supported hardware
|
|
380
|
+
|
|
381
|
+
| Device | Unit archetype | Client | Driver location |
|
|
382
|
+
| -------------------------------------------- | ----------------- | ----------------------------------- | ---------------------------------------------------------------------------- |
|
|
383
|
+
| Allen-Bradley PowerFlex 753 | `MotorUnit` | EtherNet/IP (`EipSession`) | `drivers/vsd/powerflex/pf753/` |
|
|
384
|
+
| Allen-Bradley PowerFlex 755 | `MotorUnit` | EtherNet/IP (`EipSession`) | `drivers/vsd/powerflex/pf755/` |
|
|
385
|
+
| Allen-Bradley Series 22 I/O option module | `IOUnit` | EtherNet/IP (`EipSession`) | `drivers/vsd/powerflex/series_22_io_option_module/` |
|
|
386
|
+
| Moxa ioLogik E1210 / E1211 / E1212 / E1213 / E1214 | `IOUnit` | Modbus TCP (`ModbusTcpSession`) | `drivers/io/moxa/e1200/` |
|
|
387
|
+
| Moxa ioLogik E1240 / E1241 / E1242 | `IOUnit` | Modbus TCP (`ModbusTcpSession`) | `drivers/io/moxa/e1200/` |
|
|
388
|
+
| Moxa ioLogik E1260 / E1262 | `IOUnit` | Modbus TCP (`ModbusTcpSession`) | `drivers/io/moxa/e1200/` |
|
|
389
|
+
|
|
390
|
+
## Quick example
|
|
391
|
+
|
|
392
|
+
```python
|
|
393
|
+
from procaaso_field_device.clients.ethernet_ip.client import EipSession, EipSessionConfig
|
|
394
|
+
from procaaso_field_device.drivers.vsd.powerflex.pf753.config import PowerFlex753Config
|
|
395
|
+
from procaaso_field_device.drivers.vsd.powerflex.pf753.driver import PowerFlex753Driver
|
|
396
|
+
from procaaso_field_device.units.motor import MotorUnit, MotorUnitConfig
|
|
397
|
+
|
|
398
|
+
# Client — protocol only, no device knowledge.
|
|
399
|
+
session = EipSession(EipSessionConfig(host="192.168.1.10"))
|
|
400
|
+
session.connect()
|
|
401
|
+
|
|
402
|
+
# Driver — borrows the Client, encodes vendor-specific wire format.
|
|
403
|
+
drive = PowerFlex753Driver(session, PowerFlex753Config())
|
|
404
|
+
|
|
405
|
+
# Unit — abstract motor vocabulary, vendor-neutral call sites.
|
|
406
|
+
motor = MotorUnit(MotorUnitConfig(), driver=drive)
|
|
407
|
+
|
|
408
|
+
motor.command_start()
|
|
409
|
+
motor.command_set_setpoint(50.0) # Hz, abstract
|
|
410
|
+
# ...
|
|
411
|
+
motor.command_stop()
|
|
412
|
+
|
|
413
|
+
# Escape hatch for driver-specific reads:
|
|
414
|
+
faults = motor.drv.read_fault_queue_count()
|
|
415
|
+
|
|
416
|
+
session.disconnect()
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
For a runnable bench-test version of this flow, see
|
|
420
|
+
`examples/motor_unit_test_pf753.py`.
|