Skip to content
AI-driventedge-dot is developed largely by AI coding agents, and it is alpha software. Check what it does before you rely on it, and report what you find.

Reducing data volume on a metered link

A Modbus register polled every 2 seconds produces 43,200 samples a day, and each becomes a measurement in the cloud, even when the value never moves. On a cellular or satellite link you pay for every one of them.

What you’ll build: a reporting policy that sends a temperature only when it moves by more than its noise, a status bit only when it flips, a heartbeat that keeps the device available in Cumulocity, and a way to check what the connector actually applies.

The report table is applied by the SDK runtime, in front of the sample topic, the same way for every connector and in both the Rust and the C build. A reading the policy holds back never reaches the broker, so every consumer (the measurement, alarm, event and parameter flows, and any tool subscribed to the samples) sees the reduced stream. Without a report anywhere, every reading is published.

Some readings are always published, whatever the policy: the first one after a start, a reload, a reconnect, a write or a restored broker connection, and any reading whose quality changed. A failing read is therefore reported at once, and so is the recovery.

  1. Keep the heartbeat. Cumulocity marks a device unavailable when it sends nothing within its required interval (often 60 minutes), and a device that publishes only on change can be quiet for hours. The packaged Modbus, OPC UA, CANopen and PROFIBUS configs already set a 30-minute heartbeat on the connector:

    packaging/config/modbus.toml
    report = { max_interval = "30m" }
    View packaging/config/modbus.toml on GitHub

    A heartbeat is always a fresh reading (the next poll, or an on-demand read of a pushed OPC UA node), never an old value sent again. A CAN frame signal or an SNMP trap cannot be read on demand, so it gets no heartbeat.

  2. Filter the analog values with a deadband. A temperature that moves by 0.1 °C of noise does not need to be sent. A deadband publishes a reading only once it is far enough from the last published value, so a slow drift still gets through when it adds up. Add a min_interval to cap the rate: a change that arrives too soon is held and published when the interval ends, with its own timestamp, so the last change of a burst is never lost.

    The demo library’s boiler temperature does both:

    demo/points.d/modbus/demo-sim.toml
    [[point]]
    id = "temp_scaled"
    name = "Boiler temperature"
    description = "Measured boiler loop temperature, scaled to °C"
    datatype = "uint16"
    address = { table = "holding", address = 3, count = 1 }
    transform = { decimal_shift = -3 }
    unit = "°C"
    report = { deadband = 0.5, min_interval = "10s" }
    View demo/points.d/modbus/demo-sim.toml on GitHub

    For a value whose scale varies a lot, such as a flow rate, a percent deadband fits better: report = { deadband = "2%" }.

  3. Send states on change. A status word, a mode or a coil matters when it flips: report = { on_change = true }. Booleans, strings and 64-bit integers compare exactly.

  4. Debounce what chatters. A float switch that bounces as it changes state: report = { debounce = "2s" } publishes the new state once it has held for 2 seconds, and a short-lived value is never sent.

  5. Set defaults once. report can be set on [connector], on a [[device]] and on a point (in a point library or in the configuration). The effective policy is merged key by key, the most specific level winning for each key it sets. A duration of "0" switches off an inherited setting, and on_change = false with deadband = 0 switches off change detection.

Fewer messages, or a trend instead of dropped readings

Section titled “Fewer messages, or a trend instead of dropped readings”

A policy decides which readings are sent. With measurement output (output = "measurements" in [connector]), the connector also decides how many messages carry them: it publishes thin-edge measurements itself, one per device and poll for polled points, so the mapper converts one message where it converted one per reading. The point’s batch table chooses how readings are grouped: by exact time (the default), by a frame of a few hundred milliseconds for pushed points, or by sample at a fixed rate.

When a policy would drop readings you still want to account for, a sample policy can publish the time-weighted mean, minimum and maximum of each period instead:

[[device]]
name = "line1"
batch = { aggregate = ["mean", "min", "max"], every = "1m" }
report = { max_interval = "0" } # switch off the heartbeat inherited from [connector]

An aggregate uses every reading, so a point with aggregate whose merged report policy still filters is refused when the configuration loads. The packaged configs set a heartbeat on [connector], hence the max_interval = "0"; the aggregate’s own period keeps the device available. The batching view of the config workbench shows each policy on sample readings, and the measurement output reference has the rules.

The service picks up a changed config file on its own within a few seconds, or at once with sudo systemctl reload tedge-dot. The connector then publishes the policies it applies in its retained capability descriptor, under reports:

Terminal window
tedge mqtt sub 'te/device/main/service/tedge-dot-modbus/ot/capabilities' # retained: shown at once

default is the [connector] table, devices the tables declared on devices, and points the effective, merged policy of each point that differs from its device. When a point is quieter than you expect, look there first. A sample’s seq counts published samples only, so a gap in seq still means a sample was lost on the way, never that the policy held it back.