Live streams

Collector: Modbus

Poll Modbus TCP and RTU devices with a register map, or SunSpec inverters with no map.

Modbus TCP / RTU

Modbus is the common language of meters, drives, packaged skids, battery systems and solar inverters. The collector is a Modbus master (client): it opens the connection to the device, polls the registers you list in a register map, and publishes every poll as a sample. It speaks Modbus TCP, Modbus RTU on a serial line and RTU framing carried over TCP (serial-to-Ethernet gateways). Modbus has no way to list what a device offers, so you supply the map; for solar inverters that follow SunSpec there is a preset that builds it for you.

Configuration

collector.yaml
sources:
  - name: Compressor PLC
    type: modbus
    transport: tcp
    host: 10.20.0.15
    port: 502
    unit_id: 1
    poll_ms: 1000
    map: registers.csv

  - name: Boiler room (RS-485)
    type: modbus
    transport: rtu
    serial_port: COM3          # /dev/ttyUSB0 on Linux
    baudrate: 19200
    parity: E
    map: boiler.csv

  - name: Rooftop inverter
    type: modbus
    host: 10.20.0.40
    unit_id: 126
    preset: sunspec            # no map needed
NameTypeDescription
transporttcp | rtu | rtu-over-tcp
Default: tcp
rtu is a serial line; rtu-over-tcp is RTU framing through a TCP gateway (many RS-485 converters).
hostrequiredstringDevice or gateway address (tcp and rtu-over-tcp).
portnumber
Default: 502
TCP port.
serial_portrequiredstringCOM3, /dev/ttyUSB0 (rtu only).
baudrate, parity, stopbits, bytesizenumber, N|E|O, number, number
Default: 9600, N, 1, 8
Serial settings; they must match the device exactly.
unit_idnumber
Default: 1
Default unit (slave) id for rows that leave unit_id empty. A TCP gateway in front of several serial devices routes by this id.
timeout_s, retriesnumber
Default: 3.0, 3
Per request. A request that still fails turns its tags into gaps.
poll_msnumber
Default: 1000
Default poll interval; a row can set its own.
max_registers_per_readnumber
Default: 125
Largest single read. Some devices accept fewer (often 60 or 100): lower it if you get exception 3 or 2 on long reads.
max_gapnumber
Default: 8
Registers that may be read and thrown away to merge two nearby tags into one request. 0 merges only touching tags.
mappathThe register map CSV, relative to collector.yaml. Required unless preset is set.
presetsunspecBuild the map from the device itself (SunSpec inverters and meters).

The register map

One row per tag. Column order does not matter; only name, address and type are required.

NameTypeDescription
namerequiredstringThe channel name in Kymoscope. Unique within the source.
unit_idnumber
Default: the source unit_id
Which device on the line.
tableholding | input | coil | discrete
Default: holding
Function codes 3, 4, 1 and 2. Coils and discrete inputs are type bool.
addressrequirednumberSee Addressing below.
typerequiredint16 uint16 int32 uint32 int64 uint64 float32 float64 bool bitfieldbool on a register is true when the register is not 0, or the bit selected in the bit column. bitfield is the raw 16-bit word as a number (or one bit with the bit column).
word_orderbig | little
Default: big
Multi-register values: big means the high word comes first (AB CD).
byte_orderbig | little
Default: big
Bytes inside each register: little swaps them (BA).
scale, offsetnumber
Default: 1, 0
value = raw x scale + offset. Use them for 0.1 degC steps and unit changes.
unitstringEngineering unit shown on the channel.
poll_msnumberThis tag's interval; tags with the same unit, table and interval are read together.
descriptionstringFree text.
bitnumber 0-15Optional extra column: a bool taken from one bit of a holding or input register.

Addressing

The address column is the 0-based protocol address, the number that travels on the wire. Most device manuals use the older Modicon numbering instead, where the table is part of the number and counting starts at 1: holding register 40001 is protocol address 0. The collector accepts that convention, so you can copy numbers from the manual:

In the manualTableWrite in the map
40001 to 49999 (or 400001 to 465536)holding40001 (or protocol address 0)
30001 to 39999 (or 300001 to 365536)input30001 (or protocol address 0)
10001 to 19999 (or 100001 to 165536)discrete10001 (or protocol address 0)
00001 and upcoilthe number minus 1 (always 0-based)
Protocol address (0 to 39999)anyas is

Examples

A PLC

registers.csv
name,unit_id,table,address,type,word_order,byte_order,scale,offset,unit,poll_ms,description
Suction pressure,,holding,40001,int16,,,0.01,,bar,,
Discharge temp,,holding,40002,int16,,,0.1,,degC,,
Motor speed,,holding,40003,uint16,,,,,rpm,200,Fast loop
Run hours,,holding,40010,uint32,big,big,,,h,60000,
Flow,,holding,40012,float32,little,big,,,m3/h,,Word-swapped float
Running,,coil,0,bool,,,,,,,
High pressure trip,,discrete,3,bool,,,,,,,
Alarm word,,holding,40020,bitfield,,,,,,,
Overload,,holding,40020,bool,,,,,,,Bit 3 of the alarm word

Fast and slow tags share one connection; the collector reads each interval group on its own schedule and merges neighbouring registers (40001 and 40002 become one request). Reading 40020 as both an alarm word and a single bit costs one read.

An energy meter

meter.csv
name,unit_id,table,address,type,word_order,byte_order,scale,offset,unit,poll_ms,description
Voltage L1,3,input,30001,float32,big,big,,,V,,
Voltage L2,3,input,30003,float32,big,big,,,V,,
Voltage L3,3,input,30005,float32,big,big,,,V,,
Active power,3,input,30053,float32,big,big,,,W,,
Energy import,3,input,30073,float32,big,big,0.001,,kWh,10000,Meter reports Wh

Many meters expose IEEE floats in two input registers. If your values come out as 1.5e-39 or 4e+38, the word order is the other one (see below).

A solar inverter with SunSpec

SunSpec-compliant inverters (SMA, SolarEdge, Fronius, Huawei and others, with Modbus TCP enabled) describe themselves: a marker SunS at register 40000 (the collector also tries 0 and 50000) followed by a chain of models. Set preset: sunspec and omit map. The collector scans the chain, leaves out the points the inverter marks as not implemented, applies the per-model scale factors on every poll and names the channels for you. A point that reads as not implemented later, or whose scale factor is not valid, is stored as a gap. When two models carry the same points (an inverter with both 103 and 113, or a model twice) the later one's channels get the model number in front, such as m113.Inverter.W. Use discover to see what it found before collecting.

Shell
# write the channels the inverter offers into tags.csv, to see what the collector found
kymoscope-collector discover --config collector.yaml --source "Rooftop inverter" --out tags.csv

# tags.csv then lists:
# Inverter.A, Inverter.W, Inverter.Hz, Inverter.WH, MPPT 1.DCV, MPPT 2.DCV ...
ModelChannels
101, 102, 103 (inverter, integer)Inverter.A, AphA to AphC, line and phase voltages, W, Hz, VA, VAr, PF, WH (lifetime energy), DCA, DCV, DCW, TmpCab, TmpSnk, St
111, 112, 113 (inverter, float)The same points as floats
120 (nameplate)Nameplate.WRtg, Nameplate.VARtg
160 (multiple MPPT)MPPT n.DCA, DCV, DCW, DCWH, Tmp for each module

Not read: model 1 (identification strings only), 122 to 124 (status, settings, controls), meters (201 to 204), storage and the rest. Their data is reachable with a normal map.

Word order and other pitfalls

  • Word order. A 32-bit float 1.5 is the bytes 3F C0 00 00. Devices that send the registers as 3FC0 0000 are big/big (the default). Devices that send 0000 3FC0 are word_order: little. Devices that send C03F 0000 are byte_order: little. The manual rarely says it; compare a value you can read on the device display and try the four combinations.
  • 64-bit values follow the same rule across four registers.
  • Signed vs unsigned. A temperature of -5 shown as 65531 is an int16 read as uint16.
  • Scale. Devices usually hold tenths or hundredths as integers; put the factor in scale and the unit in unit.
  • Unit id. Many TCP devices ignore it; gateways in front of serial devices need the right one. Id 255 and 0 are special on some gear.
  • Counters. 16-bit counters wrap; use the 32-bit register when the device has one.
  • Serial lines carry one conversation at a time; the collector reads one request after another per line. A line of 9600 baud moves roughly 10 registers per 100 ms: do not ask for faster polling than the line can carry (the health stream shows overruns when it cannot keep up).

What a gap means, and health

  • Every sample is stamped with the time the response arrived.
  • When a request fails (timeout, connection lost) its tags are stored as gaps (Bad status), never as the last value. The collector reconnects on its own with a growing delay up to one minute.
  • When the device answers with an exception code the same happens for the tags in that request, the code is counted, and the first time a tag fails the log names it once. An illegal data address in a merged read makes the collector read that group register by register from then on, so one wrong address costs one tag, not its neighbours.
Health stream channels for a Modbus source
connected, reconnects, polls, poll_errors, overruns, last_poll_s, values_per_s,
exceptions_1 (illegal function), exceptions_2 (illegal data address),
exceptions_3 (illegal data value), exceptions_4 (device failure), exceptions_6 (busy) ...

Troubleshooting

SymptomLikely cause
Every poll fails, connected stays 0Wrong host or port, firewall, a serial port in use by another program, wrong baudrate or parity. Some devices accept only one TCP connection: close the vendor tool.
Connects, all tags are gaps, poll_errors growsWrong unit id, or the gateway does not forward it. Timeout too short for a slow serial line: raise timeout_s.
exceptions_2 (illegal data address)The address does not exist: check 0-based versus 1-based, the table (holding vs input), and the manual's firmware version. The log names the tag once.
exceptions_3 (illegal data value)Request too long for this device: lower max_registers_per_read and max_gap.
exceptions_6 (busy)The device is overloaded by polling: raise poll_ms.
Values look random or hugeWord or byte order, or signed/unsigned.
Values are the neighbour'sOff by one register (see Addressing).
SunSpec scan says no markerThe inverter has Modbus TCP disabled, uses another unit id (SunSpec inverters often answer on 1, some on 126) or a different base. Set unit_id and retry.

discover and backfill

discover writes the tags of the map (or the SunSpec scan) as a tags file; Modbus cannot list a device. backfill does not apply: a Modbus device does not keep history, so a gap while the collector is down stays a gap (the collector's own disk buffer covers outages of the link to Kymoscope).

Permissions to ask the site for

  • TCP port 502 (or the device's port) from the collector host to the device or gateway. The collector only reads (function codes 1 to 4); it never writes to a device.
  • Modbus has no authentication or encryption. Keep it on the customer network and let only the collector host reach the device; the data leaves your site encrypted.
  • For serial: a free RS-485 adapter and access to its port (on Linux, membership of the dialout group). Install with pip install "kymoscope-collector[modbus]".