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
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| Name | Type | Description |
|---|---|---|
transport | tcp | rtu | rtu-over-tcpDefault: tcp | rtu is a serial line; rtu-over-tcp is RTU framing through a TCP gateway (many RS-485 converters). |
hostrequired | string | Device or gateway address (tcp and rtu-over-tcp). |
port | numberDefault: 502 | TCP port. |
serial_portrequired | string | COM3, /dev/ttyUSB0 (rtu only). |
baudrate, parity, stopbits, bytesize | number, N|E|O, number, numberDefault: 9600, N, 1, 8 | Serial settings; they must match the device exactly. |
unit_id | numberDefault: 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, retries | numberDefault: 3.0, 3 | Per request. A request that still fails turns its tags into gaps. |
poll_ms | numberDefault: 1000 | Default poll interval; a row can set its own. |
max_registers_per_read | numberDefault: 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_gap | numberDefault: 8 | Registers that may be read and thrown away to merge two nearby tags into one request. 0 merges only touching tags. |
map | path | The register map CSV, relative to collector.yaml. Required unless preset is set. |
preset | sunspec | Build 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.
| Name | Type | Description |
|---|---|---|
namerequired | string | The channel name in Kymoscope. Unique within the source. |
unit_id | numberDefault: the source unit_id | Which device on the line. |
table | holding | input | coil | discreteDefault: holding | Function codes 3, 4, 1 and 2. Coils and discrete inputs are type bool. |
addressrequired | number | See Addressing below. |
typerequired | int16 uint16 int32 uint32 int64 uint64 float32 float64 bool bitfield | bool 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_order | big | littleDefault: big | Multi-register values: big means the high word comes first (AB CD). |
byte_order | big | littleDefault: big | Bytes inside each register: little swaps them (BA). |
scale, offset | numberDefault: 1, 0 | value = raw x scale + offset. Use them for 0.1 degC steps and unit changes. |
unit | string | Engineering unit shown on the channel. |
poll_ms | number | This tag's interval; tags with the same unit, table and interval are read together. |
description | string | Free text. |
bit | number 0-15 | Optional 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 manual | Table | Write in the map |
|---|---|---|
| 40001 to 49999 (or 400001 to 465536) | holding | 40001 (or protocol address 0) |
| 30001 to 39999 (or 300001 to 365536) | input | 30001 (or protocol address 0) |
| 10001 to 19999 (or 100001 to 165536) | discrete | 10001 (or protocol address 0) |
| 00001 and up | coil | the number minus 1 (always 0-based) |
| Protocol address (0 to 39999) | any | as is |
Examples
A PLC
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 wordFast 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
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 WhMany 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.
# 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 ...| Model | Channels |
|---|---|
| 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 0000arebig/big(the default). Devices that send0000 3FC0areword_order: little. Devices that sendC03F 0000arebyte_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
int16read as uint16. - Scale. Devices usually hold tenths or hundredths as integers; put the factor in
scaleand the unit inunit. - 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
overrunswhen 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.
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
| Symptom | Likely cause |
|---|---|
Every poll fails, connected stays 0 | Wrong 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 grows | Wrong 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 huge | Word or byte order, or signed/unsigned. |
| Values are the neighbour's | Off by one register (see Addressing). |
| SunSpec scan says no marker | The 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
dialoutgroup). Install withpip install "kymoscope-collector[modbus]".