Edge Agent Protocols
The agent reads from four kinds of source. Two poll live devices (Modbus TCP and OPC-UA); two watch a directory of exported files (CSV and JSON). Whatever the source, each source is tagged with a machine_identifier and every reading it produces carries a metric_name, a numeric value, a unit, a UTC timestamp, and an optional raw_tag.
A unit is always required. Any reading whose unit is empty, or whose value is not a finite number, is skipped.
Modbus TCP
The Modbus collector connects to a device over TCP and reads the registers you list. On each cycle it opens a connection, reads every configured register, converts the raw value, and closes the connection. If a register read fails, that register is skipped and the rest are still collected.
Configuration
- type: modbus
machine_identifier: "CONV-001"
connection:
host: "192.168.1.101"
port: 502
registers:
- address: 100
count: 1
register_type: holding
metric_name: motor_temperature
unit: celsius
scale: 0.1
offset: 0.0
- address: 102
count: 2
register_type: input
metric_name: motor_speed
unit: rpm
scale: 1.0
offset: 0.0
Connection keys
| Key | Required | Default | Description |
|---|---|---|---|
host | Yes | 127.0.0.1 | Device IP address or hostname. Must be a valid IPv4/IPv6 address or hostname. |
port | No | 502 | TCP port. Range 1–65535. |
registers | Yes | [] | List of registers to read (see below). |
Each entry in registers:
| Key | Required | Default | Description |
|---|---|---|---|
address | Yes | 0 | Register address, 0–65535. |
count | No | 1 | Number of 16-bit registers to read. 1 reads a single register as an integer; 2 reads a big-endian 32-bit float. Only 1 or 2 are supported. |
register_type | No | holding | holding or input. |
metric_name | Yes | unknown | The metric this register maps to. |
unit | Yes | , | Unit of measure. A register with an empty unit is skipped. |
scale | No | 1.0 | Multiplier applied to the raw value. |
offset | No | 0.0 | Added after scaling. |
raw_tag | No | , | Optional label preserved on the reading (e.g. the source register). |
How values map
Raw Modbus registers are usually integers, so use scale and offset to convert to engineering units:
value = (raw_value * scale) + offset
For example, a temperature register that returns 725 with scale: 0.1 and offset: 0.0 becomes 72.5 celsius. Each register you list becomes one reading per cycle, tagged with the source's machine_identifier and the register's metric_name and unit.
Reading Modbus requires the optional Modbus extra. Install it with uv sync --extra modbus (see Installation).
OPC-UA
The OPC-UA collector connects to an OPC-UA server and reads the node values you list. It connects anonymously unless you supply both a username and a password.
Configuration
- type: opcua
machine_identifier: "CNC-001"
connection:
endpoint_url: "opc.tcp://192.168.1.100:4840"
username: ""
password: ""
nodes:
- node_id: "ns=2;i=1001"
metric_name: spindle_temperature
unit: celsius
- node_id: "ns=2;s=Vibration.X"
metric_name: vibration_x
unit: mm/s
Connection keys
| Key | Required | Default | Description |
|---|---|---|---|
endpoint_url | Yes | opc.tcp://localhost:4840 | Server endpoint. Must use the opc.tcp:// scheme. |
username | No | , | Username for authenticated servers. Leave empty to connect anonymously. |
password | No | , | Password paired with username. |
nodes | Yes | [] | List of nodes to read (see below). |
Each entry in nodes:
| Key | Required | Default | Description |
|---|---|---|---|
node_id | Yes | , | The OPC-UA node identifier to read. |
metric_name | Yes | unknown | The metric this node maps to. |
unit | Yes | , | Unit of measure. A node with an empty unit is skipped. |
raw_tag | No | , | Optional label preserved on the reading (e.g. the node id). |
How values map
Each node's value is read and converted to a number. A value that cannot be converted to a number is skipped and logged. Each node you list becomes one reading per cycle.
Node identifiers follow standard OPC-UA formats, for example:
| Format | Example |
|---|---|
| Numeric | ns=2;i=1001 |
| String | ns=2;s=Temperature.Value |
| GUID | ns=2;g=09087e75-8e5e-499b-954f-f2a9603db28a |
If you set a username without a password (or vice versa), the agent logs a warning and connects anonymously. Reading OPC-UA requires the optional extra: uv sync --extra opcua.
CSV files
The CSV collector watches a directory for .csv files and turns each row into a reading. It reads files incrementally: it remembers how far into each file it has read, so on the next cycle it only picks up newly appended rows. This makes it a good fit for systems that append to a rolling export file.
Configuration
- type: csv
machine_identifier: "PRESS-003"
connection:
directory: "/data/press-exports"
delimiter: ","
col_timestamp: "timestamp"
col_metric: "metric_name"
col_value: "value"
col_unit: "unit"
col_raw_tag: ""
Connection keys
| Key | Required | Default | Description |
|---|---|---|---|
directory | Yes | "." | Directory to watch for .csv files. Must be a project-owned path, not a system directory. |
delimiter | No | , | Column delimiter. |
col_timestamp | No | timestamp | Column holding the reading timestamp. |
col_metric | No | metric_name | Column holding the metric name. |
col_value | No | value | Column holding the numeric value. |
col_unit | No | unit | Column holding the unit. |
col_raw_tag | No | , | Optional column mapped to raw_tag. |
File format and behavior
Files must have a header row naming the columns above. Example:
timestamp,metric_name,value,unit
2026-04-04T10:30:00Z,temperature_celsius,72.5,celsius
2026-04-04T10:30:10Z,temperature_celsius,72.8,celsius
- Timestamps should be ISO 8601. If a timestamp has no timezone offset, it is assumed to be UTC.
- Rows with a missing unit, a non-numeric value, or missing required columns are skipped and logged; the rest of the file is still processed.
- Files whose names begin with a dot are ignored.
JSON and JSONL files
The JSON collector watches a directory for .json and .jsonl files and turns each object into a reading. Unlike the CSV collector, it treats each file as a one-shot export: a file is processed once and not read again, so drop each new export in as a new file.
- A
.jsonfile may contain a single object or an array of objects. - A
.jsonlfile has one JSON object per line.
Configuration
- type: json
machine_identifier: "CONVEYOR-004"
connection:
directory: "/data/conveyor-exports"
key_timestamp: "timestamp"
key_metric: "metric_name"
key_value: "value"
key_unit: "unit"
key_raw_tag: ""
Connection keys
| Key | Required | Default | Description |
|---|---|---|---|
directory | Yes | "." | Directory to watch for .json/.jsonl files. Must be a project-owned path, not a system directory. |
key_timestamp | No | timestamp | Object key holding the reading timestamp. |
key_metric | No | metric_name | Object key holding the metric name. |
key_value | No | value | Object key holding the numeric value. |
key_unit | No | unit | Object key holding the unit. |
key_raw_tag | No | , | Optional key mapped to raw_tag. |
File format and behavior
[
{"timestamp": "2026-04-04T10:30:00Z", "metric_name": "belt_speed", "value": 1.8, "unit": "m/s"},
{"timestamp": "2026-04-04T10:30:10Z", "metric_name": "belt_speed", "value": 1.9, "unit": "m/s"}
]
- Timestamps follow the same rule as CSV: ISO 8601, and a value with no timezone offset is assumed to be UTC.
- Objects with a missing unit, a non-numeric value, or a missing required key are skipped and logged.
- Files whose names begin with a dot are ignored, and very large files are skipped to protect the agent.
How the agent sends readings
Whichever collectors you use, the agent batches readings and sends them to the Haltless ingestion endpoint over HTTPS, authenticated with your API key. A batch looks like this:
{
"readings": [
{
"machine_identifier": "CNC-001",
"timestamp": "2026-04-04T10:30:00Z",
"metric_name": "temperature_celsius",
"value": 72.5,
"unit": "celsius",
"raw_tag": "ns=2;i=1001"
}
]
}
Haltless responds with a per-reading result (accepted_count, rejected_count, and any errors), so a single malformed reading never blocks the rest of the batch. For the full ingestion contract, see the Sensor data API.
Agentless alternative: direct REST ingestion
You don't have to run the agent to get data into Haltless. If it's easier for your system to push readings itself, you can send the same batch payload directly to the Haltless ingestion endpoint over HTTPS with your API key , no local software required. This is a good fit when data already lives in a system that can make outbound HTTP requests.
See Direct ingestion for the endpoint, payload, and examples.
Next steps
- Configuration , top-level keys, buffering, and TLS
- Troubleshooting , connection, value, and timestamp problems
- Direct ingestion , the agentless REST alternative