Skip to main content

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

KeyRequiredDefaultDescription
hostYes127.0.0.1Device IP address or hostname. Must be a valid IPv4/IPv6 address or hostname.
portNo502TCP port. Range 165535.
registersYes[]List of registers to read (see below).

Each entry in registers:

KeyRequiredDefaultDescription
addressYes0Register address, 065535.
countNo1Number 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_typeNoholdingholding or input.
metric_nameYesunknownThe metric this register maps to.
unitYes,Unit of measure. A register with an empty unit is skipped.
scaleNo1.0Multiplier applied to the raw value.
offsetNo0.0Added after scaling.
raw_tagNo,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.

note

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

KeyRequiredDefaultDescription
endpoint_urlYesopc.tcp://localhost:4840Server endpoint. Must use the opc.tcp:// scheme.
usernameNo,Username for authenticated servers. Leave empty to connect anonymously.
passwordNo,Password paired with username.
nodesYes[]List of nodes to read (see below).

Each entry in nodes:

KeyRequiredDefaultDescription
node_idYes,The OPC-UA node identifier to read.
metric_nameYesunknownThe metric this node maps to.
unitYes,Unit of measure. A node with an empty unit is skipped.
raw_tagNo,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:

FormatExample
Numericns=2;i=1001
Stringns=2;s=Temperature.Value
GUIDns=2;g=09087e75-8e5e-499b-954f-f2a9603db28a
note

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

KeyRequiredDefaultDescription
directoryYes"."Directory to watch for .csv files. Must be a project-owned path, not a system directory.
delimiterNo,Column delimiter.
col_timestampNotimestampColumn holding the reading timestamp.
col_metricNometric_nameColumn holding the metric name.
col_valueNovalueColumn holding the numeric value.
col_unitNounitColumn holding the unit.
col_raw_tagNo,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 .json file may contain a single object or an array of objects.
  • A .jsonl file 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

KeyRequiredDefaultDescription
directoryYes"."Directory to watch for .json/.jsonl files. Must be a project-owned path, not a system directory.
key_timestampNotimestampObject key holding the reading timestamp.
key_metricNometric_nameObject key holding the metric name.
key_valueNovalueObject key holding the numeric value.
key_unitNounitObject key holding the unit.
key_raw_tagNo,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