Skip to content

Gateway & child devices

A gateway is a device that connects to CORE-M on behalf of other devices that cannot reach the platform themselves. The classic case is an industrial bus: a dozen Modbus RTU sensors hang off a single RS-485 segment, and one edge gateway speaks Modbus to them and CORE-M to the cloud. The gateway authenticates once and fans in telemetry for all its children, each of which appears in CORE-M as its own first-class device.

Children behind a gateway have no direct credential and no direct route to CORE-M. The gateway provides both: it is the authenticated uplink, and CORE-M maps each child’s local name to a real device_id so the child’s telemetry is stored, ruled on, and anchored exactly as if it had connected directly.

CORE-M resolves children with a route key:

{tenant_id}:{gateway_device_id}:{child_identity} → device_id

The route key ties a child’s local identity (a slot index, serial, or logical name) to its platform device, scoped to the owning gateway and tenant.

flowchart LR
    subgraph Field
      C1["Child: modbus-17<br/>(temp sensor)"]
      C2["Child: modbus-18<br/>(flow meter)"]
      C3["Child: modbus-19<br/>(valve)"]
    end
    C1 -- Modbus RTU --> GW["Gateway G1<br/>(authenticated device)"]
    C2 -- Modbus RTU --> GW
    C3 -- Modbus RTU --> GW
    GW -- "HTTP / gRPC (API key)" --> CM["CORE-M gateway :8080"]
    CM --> RP[["Redpanda<br/>telemetry.raw.{tenant}"]]
    RP --> D17[("Device D17")]
    RP --> D18[("Device D18")]
    RP --> D19[("Device D19")]
  1. Register the gateway. Provision the edge device normally. It receives a device_id and an API key; this is the only credential the children need.

  2. Connect each child. The gateway calls GatewayConnectDevice with a stable child_name. CORE-M creates (or looks up) the child device and returns its child_device_id. This establishes the route for that child.

  3. Submit telemetry. The gateway calls GatewaySubmitTelemetry with a batch of points, each addressed by child_name. CORE-M resolves each name to a child device and publishes one raw telemetry point per child.

GatewayConnectDevice is idempotent: calling it again with the same child_name returns the same child. Internally the child is stored with a hardware ID of the form gw:{gateway_device_id}:{child_name}, which is what makes the lookup stable.

Terminal window
curl -X POST https://ingest.kronoxdata.com:8080/api/v1/gateway/G1/connect \
-H "Authorization: Bearer sk_live_gateway_key..." \
-H "Content-Type: application/json" \
-d '{
"gateway_device_id": "G1",
"child_name": "modbus-17"
}'
{
"child_device_id": "d17b1c0e-2f44-4a91-9b2e-2c5a1f0e9d17"
}

GatewaySubmitTelemetry fans a batch in. Each point carries the child_name, an optional timestamp (the server uses now if absent), and the numeric/string readings. CORE-M resolves each name and publishes one raw point per child, attributed to the child’s device_id for storage, rules, and anchoring.

Terminal window
curl -X POST https://ingest.kronoxdata.com:8080/api/v1/gateway/G1/telemetry \
-H "Authorization: Bearer sk_live_gateway_key..." \
-H "Content-Type: application/json" \
-d '{
"gateway_device_id": "G1",
"points": [
{
"child_name": "modbus-17",
"timestamp": "2026-05-29T14:03:00Z",
"numeric_values": { "temperature": 41.2 }
},
{
"child_name": "modbus-18",
"numeric_values": { "flow_rate": 12.7 }
}
]
}'

The response counts points across the batch, the same accepted / rejected shape as direct ingest:

{
"accepted": 2,
"rejected": 0
}
sequenceDiagram
    participant GW as Gateway G1
    participant CM as CORE-M gateway :8080
    participant Reg as Device registry
    participant RP as Redpanda telemetry.raw.{tenant}

    GW->>CM: POST /api/v1/gateway/G1/telemetry (batch by child_name)
    CM->>CM: Verify caller device_id == G1
    loop each point
        CM->>Reg: Resolve {tenant}:G1:child_name → device_id
        alt route exists (or auto_create)
            CM->>RP: Publish point as child device_id
        else unknown child, auto_create=false
            CM->>CM: Reject (corem_gateway_child_rejections_total +1)
        end
    end
    CM-->>GW: { accepted, rejected }

What happens when telemetry arrives for a child_name that has no route yet depends on the auto_create flag:

  • auto_create = true — CORE-M creates the child device on the fly from the first telemetry, so you can skip the explicit GatewayConnectDevice step.
  • auto_create = false — telemetry for an unknown child is rejected, and corem_gateway_child_rejections_total{reason="unknown_child"} is incremented. Use this when you want children to exist only after a deliberate connect.

A gateway’s children share its fate on the uplink. If the profile policy says child status follows the gateway, then when the gateway goes offline, the offline checker marks every child routed through it offline too, and publishes one device.status event per affected child. This keeps the dashboard honest — a bus of sensors does not show as online when the only thing that can reach them has dropped off.

Independently, each child still flips to online on its own first telemetry, and to offline after the offline threshold (default 120 seconds) of silence.

Reach for a gateway when…

  • Children speak a field bus (Modbus RTU, RS-485, BACnet) and can’t reach the cloud.
  • You want one credential and one uplink for a cluster of nearby devices.
  • You need each child to remain a first-class device for rules, charts, and anchoring.
  • An edge box already aggregates local sensors and can forward batches.

For devices that can reach CORE-M directly, connect them with HTTP, MQTT, CoAP, or LwM2M instead.