2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00
2026-03-02 17:48:55 -05:00

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

Quick start

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

Interactive API docs: http://<pi-ip>:8000/docs

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)

Config file

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
    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

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

sequences:
  - name: actuate
    description: "Open valve, verify sensor, close valve"
    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: 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.

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 module types

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

T1H-EBC100 hardware quirks

Unified coil address space

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".

Example: slot 1 = 8-pt input, slot 2 = 8-pt input, slot 3 = 16-pt output:

Slot Module Points Coil addresses
1 T1H-08TDS (input) 8 07
2 T1H-08TDS (input) 8 815
3 T1K-16TD2-1 (output) 16 1631

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.

FC02 input reads start at address 0

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.

No exception on out-of-range addresses

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.

FC05 write echo

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.

Unit ID is ignored

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.

Description
Sequencer for TerminatorIO
Readme 182 KiB
Languages
Python 82.2%
JavaScript 11.6%
CSS 4.9%
HTML 1.3%