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.
Files changed (89) hide show
  1. procaaso_field_device-0.0.1/LICENSE +21 -0
  2. procaaso_field_device-0.0.1/PKG-INFO +420 -0
  3. procaaso_field_device-0.0.1/README.md +396 -0
  4. procaaso_field_device-0.0.1/pyproject.toml +79 -0
  5. procaaso_field_device-0.0.1/src/procaaso_field_device/__init__.py +7 -0
  6. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/__init__.py +16 -0
  7. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/PROTOCOL_NOTES.md +481 -0
  8. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/__init__.py +15 -0
  9. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/client.py +697 -0
  10. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/codec.py +181 -0
  11. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/messaging.py +417 -0
  12. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/ethernet_ip/transport.py +709 -0
  13. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/PROTOCOL_NOTES.md +414 -0
  14. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/__init__.py +21 -0
  15. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/client.py +719 -0
  16. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/codec.py +329 -0
  17. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/messaging.py +808 -0
  18. procaaso_field_device-0.0.1/src/procaaso_field_device/clients/modbus_tcp/transport.py +244 -0
  19. procaaso_field_device-0.0.1/src/procaaso_field_device/common/__init__.py +6 -0
  20. procaaso_field_device-0.0.1/src/procaaso_field_device/common/diagnostics.py +66 -0
  21. procaaso_field_device-0.0.1/src/procaaso_field_device/common/exceptions.py +69 -0
  22. procaaso_field_device-0.0.1/src/procaaso_field_device/common/filtering.py +100 -0
  23. procaaso_field_device-0.0.1/src/procaaso_field_device/common/modes.py +91 -0
  24. procaaso_field_device-0.0.1/src/procaaso_field_device/common/safety.py +120 -0
  25. procaaso_field_device-0.0.1/src/procaaso_field_device/common/scaling.py +156 -0
  26. procaaso_field_device-0.0.1/src/procaaso_field_device/common/status.py +332 -0
  27. procaaso_field_device-0.0.1/src/procaaso_field_device/device/README.md +80 -0
  28. procaaso_field_device-0.0.1/src/procaaso_field_device/device/__init__.py +0 -0
  29. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/__init__.py +7 -0
  30. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/__init__.py +7 -0
  31. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/__init__.py +6 -0
  32. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/DRIVER_NOTES.md +624 -0
  33. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/__init__.py +60 -0
  34. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/base.py +873 -0
  35. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/config.py +191 -0
  36. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/constants.py +199 -0
  37. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1210.py +25 -0
  38. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1211.py +22 -0
  39. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1212.py +29 -0
  40. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1213.py +23 -0
  41. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1214.py +23 -0
  42. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1240.py +28 -0
  43. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1241.py +24 -0
  44. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1242.py +47 -0
  45. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1260.py +27 -0
  46. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/e1262.py +27 -0
  47. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/io/moxa/e1200/signals.py +122 -0
  48. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/__init__.py +8 -0
  49. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/PF755_INTEGRATION.md +637 -0
  50. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/__init__.py +25 -0
  51. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md +1452 -0
  52. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/DRIVER_NOTES.md.delete-me +5 -0
  53. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/__init__.py +29 -0
  54. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/config.py +62 -0
  55. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/constants.py +594 -0
  56. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/driver.py +2509 -0
  57. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/identity.py +59 -0
  58. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/signals.py +598 -0
  59. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/base/unit_config.py +28 -0
  60. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/DRIVER_NOTES.md +197 -0
  61. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/__init__.py +15 -0
  62. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/config.py +27 -0
  63. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/constants.py +46 -0
  64. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf753/driver.py +118 -0
  65. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/DRIVER_NOTES.md +260 -0
  66. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/DRIVER_NOTES.md.tmp +179 -0
  67. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/__init__.py +14 -0
  68. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/config.py +32 -0
  69. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/constants.py +135 -0
  70. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/driver.py +344 -0
  71. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/pf755/driver.py.new +1 -0
  72. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/__init__.py +33 -0
  73. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/config.py +65 -0
  74. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/constants.py +304 -0
  75. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/driver.py +679 -0
  76. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/signals.py +61 -0
  77. procaaso_field_device-0.0.1/src/procaaso_field_device/drivers/vsd/powerflex/series_22_io_option_module/unit_config.py +36 -0
  78. procaaso_field_device-0.0.1/src/procaaso_field_device/units/__init__.py +26 -0
  79. procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/UNIT_NOTES.md +994 -0
  80. procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/__init__.py +43 -0
  81. procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/config_schema.py +32 -0
  82. procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/driver_config.py +158 -0
  83. procaaso_field_device-0.0.1/src/procaaso_field_device/units/io/io.py +351 -0
  84. procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/UNIT_NOTES.md +786 -0
  85. procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/__init__.py +40 -0
  86. procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/config_schema.py +30 -0
  87. procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/driver_config.py +119 -0
  88. procaaso_field_device-0.0.1/src/procaaso_field_device/units/motor/driver_protocol.py +131 -0
  89. 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`.