From cec6ef8882259c8f5ee6e1fb0bc9af2fa1781ef4 Mon Sep 17 00:00:00 2001 From: noisedestroyers Date: Mon, 2 Mar 2026 17:53:24 -0500 Subject: [PATCH] updated README --- README.md | 239 ++++++++++++++++++++++++++++-------------------------- 1 file changed, 124 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 37e1c6c..800d102 100644 --- a/README.md +++ b/README.md @@ -1,162 +1,171 @@ # Arnold — Terminator I/O Server -Fast-poll Modbus TCP server for AutomationDirect Terminator I/O systems. -Reads all digital inputs at 20 Hz, exposes a REST API for signal state, -and executes timed output sequences. - -## Layout - -``` -server.py entrypoint — wires everything together -config.yaml edit this to describe your hardware -config_with_outputs.yaml example with input + output modules -runs.log JSON-lines sequence run history (created at runtime) - -arnold/ - config.py YAML loader, dataclasses, config validation - terminator_io.py Terminator I/O driver: Modbus TCP, signal cache, poll thread - sequencer.py Sequence execution engine - api.py FastAPI REST application -``` +Fast-poll Modbus TCP server for AutomationDirect Terminator I/O (T1H-EBC100). +Digital and analog I/O, REST API, timed output sequences, web UI, TUI debugger. ## Quick start ```bash -pip3 install pymodbus fastapi uvicorn pyyaml --break-system-packages -python3 server.py # uses config.yaml, port 8000 -python3 server.py --config config_with_outputs.yaml --log-level debug +pip3 install pymodbus fastapi uvicorn pyyaml textual --break-system-packages + +# REST API + web UI +python3 server.py --config config_with_outputs.yaml + +# TUI debugger (standalone, no API server) +python3 tui.py config_with_outputs.yaml ``` -Interactive API docs: `http://:8000/docs` +Web UI: `http://:8000` +API docs: `http://:8000/docs` + +## Web UI + +Three-panel layout (mirrors the TUI). Polls I/O at ~20 Hz. + +- **Inputs** — live digital (dot indicators) and analog (raw integer) values +- **Outputs** — click to toggle digital; +/- and Write for analog (0–65535) +- **Sequences** — run button, live step list with progress bar + +Responsive: 3-column on desktop, stacked on tablet/phone. + +## TUI + +Terminal debugger with the same three panels. + +| Key | Action | +|-----|--------| +| Tab | Cycle focus: Outputs / Sequences | +| Space/Enter | Toggle digital output / Run sequence | +| +/- | Adjust analog output (step 100) | +| Enter | Commit analog value | +| 0 / 1 | All digital outputs OFF / ON | +| r | Reconnect all devices | +| q | Quit | ## API | Method | Path | Description | |--------|------|-------------| -| GET | `/status` | Device comms health, poll rates, active sequence | -| GET | `/io` | All signal states (name → value/stale/updated_at) | -| GET | `/io/{signal}` | Single signal with device/slot/point/modbus_address | -| GET | `/sequences` | List sequences from config | -| GET | `/sequences/{name}` | Sequence detail with step list | -| POST | `/sequences/{name}/run` | Start a sequence → `{run_id}` (409 if one is running) | -| GET | `/runs/{run_id}` | Run result: pending/running/success/failed/error | -| GET | `/runs` | Recent run history (most recent first, `?limit=N`) | +| GET | `/status` | Device health, poll rates, active run | +| GET | `/io` | All signal states | +| GET | `/io/{signal}` | Single signal + metadata (category, value_type, modbus_space) | +| POST | `/io/{signal}/write` | Write output: `{"value": true}` or `{"value": 4000}` | +| GET | `/config/signals` | Full signal config list (for UI bootstrap) | +| GET | `/sequences` | List sequences | +| GET | `/sequences/{name}` | Sequence detail with steps | +| POST | `/sequences/{name}/run` | Start sequence (202), 409 if busy | +| GET | `/runs/{run_id}` | Run result | +| GET | `/runs` | Recent run history | -## Config file +## Config ```yaml devices: - id: ebc100_main host: 192.168.3.202 - port: 502 # default Modbus TCP port - unit_id: 1 # EBC100 responds to any unit ID over TCP; use 1 + port: 502 + unit_id: 1 poll_interval_ms: 50 modules: - - slot: 1 - type: T1H-08TDS # 8-pt 24VDC sinking input - points: 8 - - slot: 3 - type: T1K-16TD2-1 # 16-pt sourcing output - points: 16 + - { slot: 1, type: T1H-08TDS } # 8-pt digital input + - { slot: 2, type: T1H-08AD-1 } # 8-ch analog input (12-bit) + - { slot: 3, type: T1K-16TD2-1 } # 16-pt digital output + - { slot: 4, type: T1H-04DA-1 } # 4-ch analog output (12-bit) logical_io: - - name: sensor_a - device: ebc100_main - slot: 1 - point: 1 - direction: input - - name: valve_1 - device: ebc100_main - slot: 3 - point: 1 - direction: output + - { name: sensor_a, device: ebc100_main, slot: 1, point: 1, direction: input } + - { name: pressure, device: ebc100_main, slot: 2, point: 1, direction: input } + - { name: valve_1, device: ebc100_main, slot: 3, point: 1, direction: output, default_state: false } + - { name: dac_ch1, device: ebc100_main, slot: 4, point: 1, direction: output, default_value: 2048 } sequences: - name: actuate - description: "Open valve, verify sensor, close valve" + description: "Open valve, set DAC, verify sensor" steps: - - { t_ms: 0, action: set_output, signal: valve_1, state: true } - - { t_ms: 500, action: check_input, signal: sensor_a, expected: true } + - { t_ms: 0, action: set_output, signal: valve_1, state: true } + - { t_ms: 100, action: set_output, signal: dac_ch1, value: 4000 } + - { t_ms: 500, action: check_input, signal: sensor_a, expected: true } + - { t_ms: 600, action: check_input, signal: pressure, expected_value: 2000, tolerance: 100 } - { t_ms: 1000, action: set_output, signal: valve_1, state: false } ``` -**Timing:** `t_ms` is absolute from sequence T=0 (not relative delays). -Steps are sorted by `t_ms` at load time; order in the file doesn't matter. -Multiple steps with the same `t_ms` execute in file order. +`t_ms` is absolute from T=0 (not relative). Steps auto-sort by `t_ms`. +Failed `check_input` aborts immediately — remaining steps are skipped. +`wait_input` blocks until condition met or `timeout_ms` expires. -**Failure:** a failed `check_input` aborts the sequence immediately. -Remaining steps — including output resets — are skipped. -Add an explicit reset sequence (`all_outputs_off`) and call it after a failure. +## Supported modules (44 types) -## Supported module types +| Category | Examples | Address space | +|----------|----------|---------------| +| Digital input | T1H-08TDS, T1H-16ND3, T1K-08ND3, ... | coil (FC02) | +| Digital output | T1H-08TD1, T1K-16TD2-1, T1H-08TA, ... | coil (FC05/FC15) | +| Relay output | T1H-08TRS, T1H-16TRS2, T1K-08TRS | coil (FC05/FC15) | +| Analog input | T1H-08AD-1 (12-bit), T1H-16AD-2 (15-bit), ... | register (FC04) | +| Analog output | T1H-04DA-1 (12-bit), T1H-08DA-2 (15-bit), ... | register (FC06/FC16) | +| Temperature input | T1H-08THM, T1H-04RTD, T1K-08THM, T1K-04RTD | register (FC04) | -| Type | Direction | Points | -|------|-----------|--------| -| T1H-08TDS, T1K-08TDS | input | 8 | -| T1H-08ND3, T1K-08ND3 | input | 8 | -| T1H-16ND3, T1K-16ND3 | input | 16 | -| T1H-08NA, T1K-08NA | input | 8 | -| T1H-08TD1, T1K-08TD1 | output | 8 | -| T1H-08TD2, T1K-08TD2 | output | 8 | -| T1H-16TD1, T1K-16TD1 | output | 16 | -| T1H-16TD2, T1K-16TD2, T1K-16TD2-1 | output | 16 | -| T1H-08TA, T1K-08TA | output | 8 | -| T1H-08TRS, T1K-08TRS | output | 8 | +Full registry in `arnold/module_types.py`. -## T1H-EBC100 hardware quirks +--- -### Unified coil address space +## Architecture -The EBC100 maps **all modules — inputs and outputs — into a single flat -address space** ordered by physical slot number. There is no separate -"input base address" and "output base address". +``` +server.py Entrypoint: config load, poll start, default writes, uvicorn +tui.py Textual TUI debugger (standalone, no API server) +probe_terminator.py Standalone Modbus probe script (not part of package) -Example: slot 1 = 8-pt input, slot 2 = 8-pt input, slot 3 = 16-pt output: +config.yaml Input-only hardware config +config_with_outputs.yaml Input + output hardware config +runs.log JSON-lines run history (created at runtime) -| Slot | Module | Points | Coil addresses | -|------|--------|--------|----------------| -| 1 | T1H-08TDS (input) | 8 | 0–7 | -| 2 | T1H-08TDS (input) | 8 | 8–15 | -| 3 | T1K-16TD2-1 (output) | 16 | **16–31** | +arnold/ + __init__.py + module_types.py ModuleType frozen dataclass + 44-module registry + config.py YAML loader, validation, dual address space computation + terminator_io.py Modbus TCP driver, signal cache, dual-space poll thread + sequencer.py Sequence engine: timing, digital+analog set/check/wait + api.py FastAPI app: REST endpoints, static file mount for web UI -FC05/FC15 output writes must use these unified addresses. -The config loader computes `modbus_address` for every module and signal -automatically — you never write raw addresses in YAML. +web/ + index.html Single-page app shell + style.css Dark theme, responsive (desktop/tablet/phone) + app.js Vanilla JS: polling, output control, sequence runner +``` -### FC02 input reads start at address 0 +### Data flow -FC02 (read discrete inputs) returns only input bits, starting at bit index 0, -regardless of where inputs sit in the unified space. The poll thread reads -`total_input_points` bits from FC02 address 0. Because `modbus_address` for -input signals equals their FC02 bit index (inputs occupy the lowest slots in -practice), no remapping is needed. +``` +YAML config ──► config.py ──► module_types.py (resolve part numbers) + │ + ▼ + terminator_io.py + ┌─ TerminatorIO (Modbus TCP client per device) + │ FC02 read discrete inputs (digital) + │ FC04 read input registers (analog) + │ FC05/FC06 write single coil/register + │ FC15/FC16 write multiple coils/registers + └─ _PollThread (daemon, reads FC02+FC04 each cycle) + └─ IORegistry (multi-device coordinator, signal cache) + │ + ┌────────┼────────┐ + ▼ ▼ ▼ + api.py sequencer tui.py / web UI +``` -### No exception on out-of-range addresses +### Dual address spaces -The EBC100 returns zeros for any FC02 read address beyond the installed -modules — it never raises Modbus exception code 2 (illegal data address). -Module presence **cannot** be auto-detected from protocol errors. -The `modules` list in the config is authoritative. +The EBC100 has two independent flat address spaces: -### FC05 write echo +- **Coil space** (1-bit) — digital modules. FC01/FC02/FC05/FC15. Sequential by slot. +- **Register space** (16-bit) — analog/temperature modules. FC03/FC04/FC06/FC16. Sequential by slot. -`write_coil` (FC05) echoes back `True` for any address, even unmapped ones. -There is no error feedback for writes to non-existent output points. -Config validation at startup prevents invalid addresses from being used. +A digital module advances only `coil_offset`. An analog module advances only `register_offset`. They do not interfere. `config.py` computes all addresses at load time. -### Unit ID is ignored +### EBC100 quirks -The EBC100 accepts and echoes back any Modbus unit/slave ID over TCP. -Set `unit_id: 1` in the config (standard default). - -### No unsolicited push - -Modbus TCP is a strictly polled protocol; the EBC100 has no push capability. -The server polls at `poll_interval_ms` (default 50 ms = 20 Hz). -At 24 input points a single FC02 read takes ~1 ms on a local network. - -### Web interface - -The EBC100 hosts a minimal HTTP server on port 80 (firmware by Host Engineering). -It exposes IP/subnet/gateway config and serial port mode only — no I/O data. -Port 443, 503, and 8080 are closed. UDP port 502 is not active. +- Returns zeros for out-of-range reads (no Modbus exception code 2) +- FC05 echoes True for any address, even unmapped — no write error feedback +- Responds to any unit ID (echoed, not routed). Use `unit_id: 1` +- No unsolicited push — polling mandatory +- UDP 502 inactive; ports 443, 503, 8080 closed