{
  "version": "1",
  "prefix": "tenants/{tenantId}/sources/gateway/{chipId}",
  "directPrefix": "tenants/{tenantId}/devices/{deviceId}",
  "topics": [
    {
      "id": "gateway.status",
      "direction": "up",
      "scope": "gateway",
      "suffix": "status",
      "schema": "source-status",
      "qos": 0,
      "retain": true,
      "level": "core",
      "type": "sourceStatus",
      "handler": "source_status.js",
      "notes": "Presence. Set the MQTT last will to exactly {\"online\":false} on this topic, retained. Publish {\"online\":true,…} on connect and then every 60 s; a gateway whose last heartbeat is older than 180 s is shown offline. online:false also marks every device behind the gateway offline."
    },
    {
      "id": "gateway.firmware-response",
      "direction": "up",
      "scope": "gateway",
      "suffix": "firmware/response",
      "schema": "firmware-response",
      "qos": 0,
      "retain": false,
      "level": "core",
      "type": "firmwareResponse",
      "handler": "firmware_response.js",
      "notes": "Capability report on every connect (and in reply to a firmware/request without `type`); OTA progress while updating. The report REPLACES the stored capabilities wholesale: a protocol you do not list cannot be configured on this gateway, a feature flag you omit reads as unsupported."
    },
    {
      "id": "gateway.config-request",
      "direction": "up",
      "scope": "gateway",
      "suffix": "config/request",
      "schema": "config-request",
      "qos": 0,
      "retain": false,
      "level": "core",
      "type": "configRequest",
      "handler": "config_request.js",
      "notes": "Send on every boot AND every reconnect (config/push is not retained). The answer arrives on config/push; the same request also triggers macros/push and the job/config command, so ignore those if you do not implement them."
    },
    {
      "id": "gateway.config-push",
      "direction": "down",
      "scope": "gateway",
      "suffix": "config/push",
      "schema": "config-push",
      "qos": 1,
      "retain": false,
      "level": "core",
      "builder": "configPush",
      "notes": "Reply to config/request: the full device list, {\"unchanged\":true}, or one chunk of a large config. Also pushed unprompted when the user edits the gateway's devices or network settings."
    },
    {
      "id": "gateway.device-data",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/data",
      "schema": "data",
      "qos": 0,
      "retain": false,
      "level": "core",
      "type": "data",
      "handler": "data.js",
      "notes": "Telemetry for one device. Each message also refreshes the device's presence. Publishing faster than the account's minimum interval or hourly quota gets messages dropped and, if sustained, the device suspended."
    },
    {
      "id": "gateway.device-status",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/status",
      "schema": "device-status",
      "qos": 0,
      "retain": true,
      "level": "core",
      "type": "deviceStatus",
      "handler": "device_status.js",
      "notes": "Per-device reachability, retained. A device behind a gateway goes offline 90 s after it was last seen, so republish reachable:true (or data) at least every ~30 s for each healthy device."
    },
    {
      "id": "device.data",
      "direction": "up",
      "scope": "device",
      "suffix": "data",
      "schema": "data",
      "qos": 0,
      "retain": false,
      "level": "core",
      "type": "data",
      "handler": "data.js",
      "notes": "Telemetry from a directly connected device (protocol mqtt-direct), using the per-device MQTT credentials issued when the device was created."
    },
    {
      "id": "device.status",
      "direction": "up",
      "scope": "device",
      "suffix": "status",
      "schema": "device-status",
      "qos": 0,
      "retain": true,
      "level": "core",
      "type": "deviceStatus",
      "handler": "device_status.js",
      "notes": "Reachability of a directly connected device. Set the MQTT last will to {\"ts\":…,\"reachable\":false,\"reason\":\"lwt\"} on this topic, retained."
    },
    {
      "id": "gateway.data-backfill",
      "direction": "up",
      "scope": "gateway",
      "suffix": "data/backfill",
      "schema": "data-backfill",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "dataBackfill",
      "handler": "data_backfill.js",
      "notes": "Replay of telemetry buffered while offline: at most 40 records per message, oldest first, each naming its deviceId. NEVER replay on the live data topic — the live rate gate would suspend the device. Stored only: no dashboards, rules or last-known values."
    },
    {
      "id": "gateway.cmd",
      "direction": "down",
      "scope": "gateway",
      "suffix": "cmd",
      "schema": "gateway-cmd",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "gatewayCmd",
      "notes": "Commands to the gateway itself (restart, reset/config, debug, ble/scan, job/config, …). Ignore any `command` you do not recognise. job/config is re-sent after every config/request."
    },
    {
      "id": "gateway.device-cmd",
      "direction": "down",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/cmd",
      "schema": "device-cmd",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "cmd",
      "notes": "Commands to one device behind the gateway: read control, Modbus writes, actuator writes. Reply on devices/{deviceId}/cmd/ack when the body carries a correlationId."
    },
    {
      "id": "gateway.device-cmd-ack",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/cmd/ack",
      "schema": "cmd-ack",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "cmdAck",
      "handler": "cmd_ack.js",
      "notes": "Acknowledges a device command, echoing its correlationId."
    },
    {
      "id": "device.cmd",
      "direction": "down",
      "scope": "device",
      "suffix": "cmd",
      "schema": "device-cmd",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "directCmd",
      "notes": "Commands to a directly connected device (Modbus and actuator writes; read control is gateway-only)."
    },
    {
      "id": "device.cmd-ack",
      "direction": "up",
      "scope": "device",
      "suffix": "cmd/ack",
      "schema": "cmd-ack",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "cmdAck",
      "handler": "cmd_ack.js",
      "notes": "Acknowledges a command sent to a directly connected device."
    },
    {
      "id": "gateway.device-alert",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/alert",
      "schema": "alert",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "alert",
      "handler": "alert.js",
      "notes": "An alert raised at the edge for one device, e.g. a tag outside its config/push threshold band."
    },
    {
      "id": "device.alert",
      "direction": "up",
      "scope": "device",
      "suffix": "alert",
      "schema": "alert",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "alert",
      "handler": "alert.js",
      "notes": "An alert raised by a directly connected device."
    },
    {
      "id": "gateway.firmware-request",
      "direction": "down",
      "scope": "gateway",
      "suffix": "firmware/request",
      "schema": "firmware-request",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "firmwareRequest",
      "notes": "Over-the-air update request. Report progress on firmware/response. The gateway is held busy (restart/reset refused) until a terminal status or 5 minutes."
    },
    {
      "id": "gateway.debug-response",
      "direction": "up",
      "scope": "gateway",
      "suffix": "debug/response",
      "schema": "debug-response",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "debugResponse",
      "handler": "debug_response.js",
      "notes": "Diagnostics snapshot, the reply to the debug/diag gateway command. Relayed live, not stored."
    },
    {
      "id": "gateway.debug-log",
      "direction": "up",
      "scope": "gateway",
      "suffix": "debug/log",
      "schema": "debug-log",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "debugLog",
      "handler": "debug_log.js",
      "notes": "Live log lines between debug/logs/start and debug/logs/stop, filtered by the start command's category mask. Relayed live, not stored."
    },
    {
      "id": "gateway.macros-push",
      "direction": "down",
      "scope": "gateway",
      "suffix": "macros/push",
      "schema": "macros-push",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "macrosPush",
      "notes": "The complete set of macro programs for this gateway. Re-sent after every config/request; replace your whole set each time."
    },
    {
      "id": "gateway.macros-run",
      "direction": "down",
      "scope": "gateway",
      "suffix": "macros/run",
      "schema": "macros-run",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "macrosRun",
      "notes": "Start a macro run. Moves physical equipment. Report it on macro/run/status."
    },
    {
      "id": "gateway.macros-abort",
      "direction": "down",
      "scope": "gateway",
      "suffix": "macros/abort",
      "schema": "macros-abort",
      "qos": 1,
      "retain": false,
      "level": "optional",
      "builder": "macrosAbort",
      "notes": "Abort one run ({runId}) or every run ({}). A network message — not a safety function."
    },
    {
      "id": "gateway.macro-run-status",
      "direction": "up",
      "scope": "gateway",
      "suffix": "macro/run/status",
      "schema": "macro-run-status",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "macroRunStatus",
      "handler": "macro_run_status.js",
      "notes": "Run lifecycle: started, step, then done or error."
    },
    {
      "id": "gateway.macro-step",
      "direction": "up",
      "scope": "gateway",
      "suffix": "macro/step",
      "schema": "macro-step",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "macroStep",
      "handler": "macro_step.js",
      "notes": "A running macro's CALL instruction: asks the backend to run a backend step (a saved webhook/MQTT action). Fire-and-forget."
    },
    {
      "id": "gateway.job",
      "direction": "up",
      "scope": "gateway",
      "suffix": "job",
      "schema": "job-event",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "jobEvent",
      "handler": "job_event.js",
      "notes": "Job boundaries from the input configured by the job/config command. localJobId is the idempotency key — re-announce the same id after a reboot."
    },
    {
      "id": "gateway.discovery-ble",
      "direction": "up",
      "scope": "gateway",
      "suffix": "discovery/ble",
      "schema": "ble-discovery",
      "qos": 0,
      "retain": false,
      "level": "optional",
      "type": "bleDiscovery",
      "handler": "discovery_ble.js",
      "notes": "Result of a ble/scan gateway command: the Bluetooth LE sensors heard nearby."
    },
    {
      "id": "gateway.device-image-meta",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/image/meta",
      "schema": "image-meta",
      "qos": 0,
      "retain": false,
      "level": "experimental",
      "type": "imageMeta",
      "handler": "image.js",
      "notes": "Header of an event-triggered camera frame relayed by a fog node; image/chunk messages follow."
    },
    {
      "id": "gateway.device-image-chunk",
      "direction": "up",
      "scope": "gateway",
      "suffix": "devices/{deviceId}/image/chunk",
      "schema": "image-chunk",
      "qos": 0,
      "retain": false,
      "level": "experimental",
      "type": "imageChunk",
      "handler": "image.js",
      "notes": "One base64 slice of the image announced by image/meta, in idx order."
    }
  ],
  "schemas": {
    "alert": "https://synacl.com/protocol/v1/schemas/alert.json",
    "ble-discovery": "https://synacl.com/protocol/v1/schemas/ble-discovery.json",
    "cmd-ack": "https://synacl.com/protocol/v1/schemas/cmd-ack.json",
    "config-push": "https://synacl.com/protocol/v1/schemas/config-push.json",
    "config-request": "https://synacl.com/protocol/v1/schemas/config-request.json",
    "data-backfill": "https://synacl.com/protocol/v1/schemas/data-backfill.json",
    "data": "https://synacl.com/protocol/v1/schemas/data.json",
    "debug-log": "https://synacl.com/protocol/v1/schemas/debug-log.json",
    "debug-response": "https://synacl.com/protocol/v1/schemas/debug-response.json",
    "device-cmd": "https://synacl.com/protocol/v1/schemas/device-cmd.json",
    "device-status": "https://synacl.com/protocol/v1/schemas/device-status.json",
    "firmware-request": "https://synacl.com/protocol/v1/schemas/firmware-request.json",
    "firmware-response": "https://synacl.com/protocol/v1/schemas/firmware-response.json",
    "gateway-cmd": "https://synacl.com/protocol/v1/schemas/gateway-cmd.json",
    "image-chunk": "https://synacl.com/protocol/v1/schemas/image-chunk.json",
    "image-meta": "https://synacl.com/protocol/v1/schemas/image-meta.json",
    "job-event": "https://synacl.com/protocol/v1/schemas/job-event.json",
    "macro-program": "https://synacl.com/protocol/v1/schemas/macro-program.json",
    "macro-run-status": "https://synacl.com/protocol/v1/schemas/macro-run-status.json",
    "macro-step": "https://synacl.com/protocol/v1/schemas/macro-step.json",
    "macros-abort": "https://synacl.com/protocol/v1/schemas/macros-abort.json",
    "macros-push": "https://synacl.com/protocol/v1/schemas/macros-push.json",
    "macros-run": "https://synacl.com/protocol/v1/schemas/macros-run.json",
    "source-status": "https://synacl.com/protocol/v1/schemas/source-status.json"
  },
  "examples": {
    "alert": "https://synacl.com/protocol/v1/examples/alert.json",
    "ble-discovery": "https://synacl.com/protocol/v1/examples/ble-discovery.json",
    "ble-discovery.restarting": "https://synacl.com/protocol/v1/examples/ble-discovery.restarting.json",
    "cmd-ack.error": "https://synacl.com/protocol/v1/examples/cmd-ack.error.json",
    "cmd-ack": "https://synacl.com/protocol/v1/examples/cmd-ack.json",
    "cmd-ack.null-value": "https://synacl.com/protocol/v1/examples/cmd-ack.null-value.json",
    "config-push.chunk": "https://synacl.com/protocol/v1/examples/config-push.chunk.json",
    "config-push.empty": "https://synacl.com/protocol/v1/examples/config-push.empty.json",
    "config-push": "https://synacl.com/protocol/v1/examples/config-push.json",
    "config-push.unchanged": "https://synacl.com/protocol/v1/examples/config-push.unchanged.json",
    "config-push.with-net": "https://synacl.com/protocol/v1/examples/config-push.with-net.json",
    "config-request.chunked": "https://synacl.com/protocol/v1/examples/config-request.chunked.json",
    "config-request.empty": "https://synacl.com/protocol/v1/examples/config-request.empty.json",
    "config-request.first-boot": "https://synacl.com/protocol/v1/examples/config-request.first-boot.json",
    "config-request": "https://synacl.com/protocol/v1/examples/config-request.json",
    "data-backfill": "https://synacl.com/protocol/v1/examples/data-backfill.json",
    "data": "https://synacl.com/protocol/v1/examples/data.json",
    "data.mixed-types": "https://synacl.com/protocol/v1/examples/data.mixed-types.json",
    "debug-log": "https://synacl.com/protocol/v1/examples/debug-log.json",
    "debug-response": "https://synacl.com/protocol/v1/examples/debug-response.json",
    "device-cmd.actuator-motor": "https://synacl.com/protocol/v1/examples/device-cmd.actuator-motor.json",
    "device-cmd.actuator-set": "https://synacl.com/protocol/v1/examples/device-cmd.actuator-set.json",
    "device-cmd": "https://synacl.com/protocol/v1/examples/device-cmd.json",
    "device-cmd.modbus-coil": "https://synacl.com/protocol/v1/examples/device-cmd.modbus-coil.json",
    "device-cmd.modbus-direct-fc16": "https://synacl.com/protocol/v1/examples/device-cmd.modbus-direct-fc16.json",
    "device-cmd.read-disable-manual": "https://synacl.com/protocol/v1/examples/device-cmd.read-disable-manual.json",
    "device-cmd.read-disable-timed": "https://synacl.com/protocol/v1/examples/device-cmd.read-disable-timed.json",
    "device-cmd.read-enable": "https://synacl.com/protocol/v1/examples/device-cmd.read-enable.json",
    "device-cmd.read-once": "https://synacl.com/protocol/v1/examples/device-cmd.read-once.json",
    "device-cmd.set-interval": "https://synacl.com/protocol/v1/examples/device-cmd.set-interval.json",
    "device-cmd.stepper-move": "https://synacl.com/protocol/v1/examples/device-cmd.stepper-move.json",
    "device-status": "https://synacl.com/protocol/v1/examples/device-status.json",
    "device-status.unreachable": "https://synacl.com/protocol/v1/examples/device-status.unreachable.json",
    "firmware-request": "https://synacl.com/protocol/v1/examples/firmware-request.json",
    "firmware-response.chunk-capable": "https://synacl.com/protocol/v1/examples/firmware-response.chunk-capable.json",
    "firmware-response": "https://synacl.com/protocol/v1/examples/firmware-response.json",
    "firmware-response.ota-failed": "https://synacl.com/protocol/v1/examples/firmware-response.ota-failed.json",
    "firmware-response.ota-progress": "https://synacl.com/protocol/v1/examples/firmware-response.ota-progress.json",
    "gateway-cmd.ble-scan": "https://synacl.com/protocol/v1/examples/gateway-cmd.ble-scan.json",
    "gateway-cmd.debug-diag": "https://synacl.com/protocol/v1/examples/gateway-cmd.debug-diag.json",
    "gateway-cmd.debug-logs-start": "https://synacl.com/protocol/v1/examples/gateway-cmd.debug-logs-start.json",
    "gateway-cmd.debug-logs-stop": "https://synacl.com/protocol/v1/examples/gateway-cmd.debug-logs-stop.json",
    "gateway-cmd.job-config": "https://synacl.com/protocol/v1/examples/gateway-cmd.job-config.json",
    "gateway-cmd": "https://synacl.com/protocol/v1/examples/gateway-cmd.json",
    "gateway-cmd.reset-config": "https://synacl.com/protocol/v1/examples/gateway-cmd.reset-config.json",
    "gateway-cmd.sim-start": "https://synacl.com/protocol/v1/examples/gateway-cmd.sim-start.json",
    "gateway-cmd.sim-stop": "https://synacl.com/protocol/v1/examples/gateway-cmd.sim-stop.json",
    "image-chunk": "https://synacl.com/protocol/v1/examples/image-chunk.json",
    "image-meta": "https://synacl.com/protocol/v1/examples/image-meta.json",
    "job-event.end-uncertain": "https://synacl.com/protocol/v1/examples/job-event.end-uncertain.json",
    "job-event": "https://synacl.com/protocol/v1/examples/job-event.json",
    "macro-program": "https://synacl.com/protocol/v1/examples/macro-program.json",
    "macro-run-status.error": "https://synacl.com/protocol/v1/examples/macro-run-status.error.json",
    "macro-run-status": "https://synacl.com/protocol/v1/examples/macro-run-status.json",
    "macro-run-status.step": "https://synacl.com/protocol/v1/examples/macro-run-status.step.json",
    "macro-step": "https://synacl.com/protocol/v1/examples/macro-step.json",
    "macros-abort.all": "https://synacl.com/protocol/v1/examples/macros-abort.all.json",
    "macros-abort": "https://synacl.com/protocol/v1/examples/macros-abort.json",
    "macros-push.empty": "https://synacl.com/protocol/v1/examples/macros-push.empty.json",
    "macros-push": "https://synacl.com/protocol/v1/examples/macros-push.json",
    "macros-run": "https://synacl.com/protocol/v1/examples/macros-run.json",
    "source-status.cellular": "https://synacl.com/protocol/v1/examples/source-status.cellular.json",
    "source-status": "https://synacl.com/protocol/v1/examples/source-status.json",
    "source-status.lan": "https://synacl.com/protocol/v1/examples/source-status.lan.json",
    "source-status.lwt": "https://synacl.com/protocol/v1/examples/source-status.lwt.json"
  },
  "docs": "https://synacl.com/protocol/"
}
