# MQTT

> Devices that publish through an external MQTT broker, in plain MQTT or Sparkplug B, with the broker modeled as the node every device under it shares, and the station's own data published to a broker.

The MQTT driver reads and writes the tags of equipment that publishes through an **external MQTT broker**: a gateway, a wireless sensor, a Node-RED flow, an edge node that speaks Sparkplug B. The station is a client of that broker: it subscribes to the topics its tags read and publishes the writes you make, and each broker can publish the station's own data, the tags, Logic variables and alarms listed on its [Publish card](#the-publish-card), as JSON or in Sparkplug B. It is a client and not a broker, so the equipment and the station both connect to a broker: when you have none, the station's own [MQTT broker](connector-mqtt-broker) can be that broker, and turning it on adds the broker **This station** to this driver; [Installing a broker](#installing-a-broker) shows how to set up a separate one.

Every device behind a broker rides the same connection, so the topology is
**Driver → Broker → Device → Tag**: the **broker** owns where it answers and how the
station signs in, typed once; each **device** under it owns only how it publishes,
**Plain MQTT** or **Sparkplug B**, and its tags. Both protocols live side by side under
one broker. Values arrive by subscription, not by polling, so no MQTT device or tag has a
poll interval.

## The driver's page {#the-drivers-page}

The MQTT driver's page lists brokers instead of devices. The line under its header reads
"Each broker is one connection to an external MQTT broker. Configure where it answers and how the station signs in, then add the devices that publish through it."
The **MQTT brokers** table carries **Add broker** at the right of its title and one row
per broker: **Name**, **Address** (`mqtt://host:port`, or `mqtts://host:port` over TLS,
empty while the broker has no host), **Devices**, **State** (a dot and the broker's state
with what its devices are doing, such as "Online · 2/2 devices online"), then **Open**
and, for Configure connector, **Enable**, **Disable** and **Delete**. The empty table
reads "No brokers yet." A broker that refused the station carries the tree's red marker after its name, and the page names under its header the brokers with an error ("1 broker with an error: Plant").

In the tree the driver row counts both levels ("1 broker · 2 devices"), and the page
header shows the broker count in parentheses. In the column's New… menu the MQTT entry
offers **New broker**, then each broker with its own **New device** and its devices, and
right-clicking the driver row offers **Add broker**. The driver's bar has no Discover: a
scan listens on one broker, so it lives on the broker's page. Once added, the driver
stands under the Drivers folder of the tree, with or without a broker.

## The broker {#the-broker}

**Add broker**, **New broker** in the New… menu and the driver row's right-click menu create a broker
named "New broker" (then "New broker 2") and disabled, so its connection is filled in
before the station connects. The feed says so: "Added disabled broker 'New broker'. Enter where it answers, then enable it."
The broker is not opened: it is marked in the MQTT brokers table of the driver's page, where the screen goes from New… or the right-click. Its page, reached by **Open** on its row or its row in the tree, carries the line "One MQTT broker: where it answers and how the station signs in to it. Every device under it shares this connection."
and, in the header, the state, its dot and the address. Under the header, the broker's devices that have an error of their own are named ("1 device with an error: Pump"); while the broker itself has an error, its devices point to it instead: a device's page says "This device goes through broker Plant, which has an error. The broker's page says why."

### The Broker connection card {#the-broker-connection-card}

| Field | Meaning | Default |
| --- | --- | --- |
| **Name** | The friendly name of the broker in the tree and on every device under it. It never enters an address. Blank is refused: "Broker name is required." | "New broker" |
| **Host** | The broker's host name or IP address. A whole address can be pasted: `mqtt://` or `tcp://` in front sets the transport to the clear, `mqtts://`, `ssl://` or `tls://` to TLS, a port after the host goes to **Port**, and the host is kept bare. Another scheme, a path or a sign-in written into the address is refused under the box, every field keeping what it held: "'http://192.168.0.20' is not a broker address. Write the broker's host name or IP address, optionally as mqtt://host:port or mqtts://host:port." | empty |
| **Port** | The TCP port the broker answers on, 1 to 65535. | 1883 |
| **Transport** | The checkbox **Use TLS**. On, the connection is encrypted and the broker's certificate is checked against the Windows certificate store, or against the certificates approved under [Equipment access](#equipment-access): "Encrypted: the broker's certificate has to be trusted by Windows or approved under Equipment access on the MQTT driver's page." Off, the note warns "Unencrypted: values and the password travel in the clear." Switching it moves a port left at 1883 to 8883, and back; any other port stays as it is. | off |
| **Username**, **Password** | The account the station signs in to the broker with. An empty username signs in anonymously. The password is protected at rest like a device's credentials and never appears in an address, the journal or an agent's answer. | empty |
| **Client ID** | The name the station's session carries on the broker, 1 to 128 characters with no control characters. A broker keeps one session per client ID and drops the older of two, so a client ID belongs to one broker of this station: "Client id 'GanterLab01' already belongs to broker 'Plant'. Two sessions with one client id push each other off the broker: choose another." When two stations share a broker, each keeps its own. | `GanterLab` and 14 hexadecimal digits, generated |
| **Keep-alive (s)** | How often the session proves it is alive when nothing else travels: "Keep-alive must be between 5 and 3600 seconds." | 60 |

Under the fields, a line says what the broker and its devices are doing, what the broker
still lacks, and "Every device under this broker and what the station publishes to it share this connection: the session opens when the first enabled device or published row needs it."
Every field stays editable while the broker is enabled. A saved change of where it
answers, the transport, the sign-in, the client ID or the keep-alive reconnects the one
session every device under the broker shares.

### The command bar {#the-command-bar}

| Command | What it does | Greyed when (situation) | Not drawn when (role) |
| --- | --- | --- | --- |
| **Enable** | Lets the devices under the broker use its session. A broker with no host is refused: "Enter the broker's host name or IP address before the broker is enabled." | Already enabled, the connection is not complete ("This broker's connection is not complete yet."), a scan is running, or the station refuses mutations. | Without Configure connector. |
| **Disable** | Closes the session for every device under it; each device keeps its own Enabled choice and comes back as it was when the broker is enabled again. | Already disabled, a scan is running, or the station refuses mutations. | Without Configure connector. |
| **Delete broker** (danger ink, on the right) | Asks first. An empty broker: "This broker has no devices."; one with devices: "This also deletes the broker's 2 devices and all of their tags." Then the MQTT driver is selected and the feed reads "Deleted broker 'Plant' and its 2 devices." | A scan is running ("A discovery scan is using this broker. Wait for it to finish."), or the station refuses mutations. | Without Configure connector. |

Right-clicking a broker row offers **Add device**, **Enable**, **Disable** and **Delete**.

### Broker states {#broker-states}

The broker reports its session apart from its devices, in the tree row, in the driver's
brokers table and in its own header:

| State | Dot | Meaning |
| --- | --- | --- |
| **Off** | grey | The broker is disabled. |
| **No session** | grey | Enabled, but no enabled device rides it and its Publish card publishes nothing, so no session is opened. |
| **Connecting…** | amber | A device is enabled, or the broker publishes, and the session is connecting, or reconnecting after a loss. |
| **Online** | green | The session is connected. |
| **Unreachable** | red | The broker could not be reached or refused the station. The cause is printed in red under the header in the station's language and, in the driver's words, as the row's error tooltip, in the terms of what to correct: nothing answers on that port, the name is unknown on this network, the user name or the password was refused, the certificate is not trusted or waits for approval under [Equipment access](#equipment-access), another client took the session with the same client ID. A broker that refuses the port or a client without a user name is said with its setting instead (see [Prepare the broker](#prepare-the-broker)). |

The tree row reads the state and the device count ("Online · 2 devices"), and the
summary under the card adds what the devices are doing: "Online · 2/2 devices online",
"Online · 1 online · 1 with issues", "No session · no enabled devices". A Sparkplug B
device that announced its death takes only itself off the air: the broker stays Online.
The session speaks MQTT 5 and falls back to MQTT 3.1.1 for a broker that does not, starts
clean, and reconnects by itself, waiting longer between attempts while the broker stays
away.

### The Discover devices card {#the-discover-devices-card}

Drawn only for Configure connector, under the line "Listens on this broker for a while: the Sparkplug B edge nodes and devices that announce themselves, and the topics under a filter."

| Field | Meaning | Default |
| --- | --- | --- |
| **Listen (s)** | How long the scan listens, 3 to 60 seconds. | 10 |
| **Topic filter** | The plain topics to list, as tags of one device. The wildcards `+` (one level) and `#` (this level and everything under it, last only) are allowed, so "e.g. plant/#". Empty lists Sparkplug B devices only. | empty |

**Discover** starts the scan and turns into **Cancel** while it runs. The line under the
fields says what it will do ("Listens to 192.168.0.20:1883 for 10 s."), and the progress
line counts the window ("Listening to 192.168.0.20:1883 · 4 of 10 s…"). The scan needs a
host stored on the broker: until then the card says "Enter the broker's host and let the edit save before discovery."
A pending edit on the broker is saved first; when it cannot be, nothing starts:
"Discovery was not started because the broker edit could not be saved."

What a scan does on the broker:

- It connects under a client ID of its own, so it never pushes the devices' session off
  the broker, and it may run while the broker is enabled and in use.
- It subscribes to every Sparkplug B topic (`spBv1.0/#`) and to the topic filter.
  Sparkplug B birth certificates are not retained, so an edge node heard without one is
  asked to announce itself again (the `Node Control/Rebirth` command, the only message the
  scan publishes); an edge node that is running answers inside the window.
- Each edge node and each device behind it that announces itself (NBIRTH, DBIRTH) becomes
  a candidate with its metrics as tags, typed by what the birth declares. The edge node's
  own plumbing (`bdSeq` and the `Node Control/…` and `Device Control/…` metrics) is left
  out, and so are the metrics no tag reads (arrays, data sets, templates, property sets,
  bytes, files), counted under their reason. An edge node heard but not born within the
  window is offered with no tags.
- With a topic filter, the plain topics heard under it become one candidate of their own:
  each topic is a tag, or, for a JSON payload, each value inside it is one by its value
  path (up to 32 per topic, four levels deep, the first 8 elements of an array). A number
  is offered as Double, `true` or `false` as Boolean, anything else as String. The list
  stops at 500 topics. A topic whose publisher retains its last message is heard at once;
  any other only if it publishes during the window.
- Nothing is saved until you add a candidate. Cancel ends the window at once.

The results card, **Discovered devices**, lists **Publishes as** (`group/edge node`, with
`/device` behind an edge node, or "Plain topics"), **Device** (the name to add it under,
editable: the device or edge node ID, or the levels of the filter before its first
wildcard), **Result** ("12 of 12 tags selected") and **Add device**, which creates the
device under this broker with those tags. A Sparkplug B identity already configured on
the broker reads "Already configured" and offers **Open** instead. A scan that heard
nothing says "Nothing announced itself on this broker while the scan listened."

A device you already have finds what it does not read yet from its own page: **Discover tags**, on the head of its Tags card, listens on its broker for 10 s. A Sparkplug B device hears its own edge node alone, which is asked for its birth while no other edge node is asked anything; a plain device hears the topics under the levels its tags share (`plant/oven/#` for `plant/oven/temperature` and `plant/oven/count`), and every topic on the broker when they share none. Only the values it has no tag for yet are offered (see [Discovering a device's tags](connector-discovery#discovering-a-devices-tags)).

### The Devices card {#the-devices-card}

"Each device reads plain topics, or one Sparkplug B edge node or device. The connection is inherited from the broker and cannot be changed here."
**Add device** creates "New device" under this broker, enabled, in Plain MQTT, marked in this
table and scrolled into view: "Added device 'New device' on 'Plant'." The table lists **Device**, **Publishes as**,
**Tags** and **Health**, with Open, Enable, Disable and Delete on each row; empty, it reads
"No devices configured on this broker." A device with an error of its own carries the tree's red marker after its name.

### The Publish card {#the-publish-card}

The last card publishes the station's own data to this broker, under the line "The station publishes each row to this broker as JSON on its topic whenever the value or its quality changes, and every row again each time the connection opens. The broker keeps the last message of each topic for whoever subscribes later. The station subscribes only to the write topic of each row set to Read and write."
Viewing the Connector reads the card; configuring it changes it. The broker This station of
the station's own [MQTT broker](connector-mqtt-broker) carries the same card.

| Field | Meaning | Default |
| --- | --- | --- |
| **Publication format** | How the rows go out: **JSON** on each row's topic ([Publishing](#publishing)), or **Sparkplug B**, the station an edge node of the broker ([Publishing in Sparkplug B](#publishing-in-sparkplug-b)). Its tooltip: "How the rows go out: JSON on each row's own topic, or Sparkplug B, with this station as an edge node that Ignition and other Sparkplug B consumers find on their own." | JSON |
| **Topic prefix** | JSON only. The level the topic of each new row is suggested under. Empty, the field shows the default and uses it. "The topic prefix cannot hold + or #, start with $ or hold a character no topic may hold." Changing it leaves the topics already in the list as they are. | `ganterlab/` and the station's name |
| **Group ID** | Sparkplug B only, in the prefix's place. The Sparkplug B group of the station's edge node on this broker; empty, it is `GanterLab`. A group holding `/`, `+` or `#` is refused under the field, and the broker is not saved until it is corrected. | `GanterLab` |

In Sparkplug B the line under the title says instead "The station is a Sparkplug B edge node on this broker: Logic variables and alarms are its own metrics, and each device with rows is a Sparkplug B device. It announces every metric when the connection opens and whenever the list changes, then publishes each value or quality that changes. Only a row set to Read and write takes a value from a command.", and under the fields **Edge node ID (the station's name)** shows the edge node it publishes as.

The table lists **Source**, **Topic** and **Access**, by topic; a row whose source is not chosen yet reads
"No source yet". With none, it reads "Nothing is published to this broker yet. Add a row with New row."
In Sparkplug B the **Topic** column is **Metric**: a tag after its device (`Oven · Temperature`), a
variable or an alarm by its name on the edge node (`Logic/Oven/Setpoint`, `Alarms/High`).
While the broker is disabled and the list has rows, a note says "The broker is disabled, so nothing is published until it is enabled."
While a row of the broker is set to Read and write, a second note says "Rows set to Read and write take values published on their write topic. Anyone who can publish to this broker can change those sources, so the broker's username and password are their only guard." In Sparkplug B it says "Rows set to Read and write take values from Sparkplug B commands (NCMD, DCMD). Anyone who can publish to this broker can change those sources, so the broker's username and password are their only guard."

| Command | What it does | Greyed when (situation) | Not drawn when (role) |
| --- | --- | --- | --- |
| **New row** | Adds a row that publishes nothing yet and selects it: "Row added to the broker's Publish list. Pick its source." | A run holds the station, or the runtime is stopped. | Without Configure connector. |
| **Delete row** (danger ink) | Deletes the selected row at once, its tooltip naming what it publishes. The station stops publishing it; the message the broker retained on its topic stays there. | No row is selected ("Select a row of the list to delete it."), a run holds the station, or the runtime is stopped. | Without Configure connector. |

A row's fields are stored the moment they change:

| Field | What it does |
| --- | --- |
| **Source** | What the row publishes: "(none)", then every enabled tag of an enabled device ("Tag · Simulated · Oven · Temperature"), every Logic variable that holds a value and every alarm, named by its folders. A device command publishes nothing and is not offered. The row holds its source by identity, so a rename keeps the row on it, and the source's [Used by](connector-tags#the-used-by-card) card lists the row. |
| **Topic** | JSON only. Where the row publishes. Picking a source suggests a topic from its name: `<prefix>/<device>/<tag>` for a tag, `<prefix>/logic/<folders>/<variable>` for a variable, `<prefix>/alarms/<folders>/<alarm>` for an alarm, each name one topic level (a space, a slash or a wildcard becomes `_`). Edit it freely: a topic still the one suggested follows a newly picked source, one you wrote stays, and renaming the source keeps it. |
| **Access** | **Read only**, the default of every row: the station publishes the row and subscribes to nothing for it. **Read and write**: the station also takes the values published on the row's write topic, its topic followed by `/set`, or in Sparkplug B the values a command sends to its metric, and writes them to the source ([Writing back](#writing-back)). It is offered only for a tag with write access on a driver that writes, or a memory variable; elsewhere it stays listed, greyed: "This source takes no write: only a tag with write access or a memory variable does." Picking an alarm sets the row back to read only. |

Under the fields a line says what the row publishes:

| The card says | When |
| --- | --- |
| "This row publishes nothing yet: pick its source." | No source is chosen. |
| "Publishes the value, its quality and its time as JSON whenever the value or the quality changes." | A tag or a variable, read only. |
| "Publishes the value, its quality and its time as JSON whenever the value or the quality changes, and writes to the source each value published on the write topic: each write is recorded in the journal with the broker's name." | A tag or a variable set to Read and write. |
| "Publishes whether the alarm is active and acknowledged, its state, priority, message and time as JSON whenever one of them changes." | An alarm. |
| "The source of this row no longer exists, so nothing more is published for it. The broker keeps the last message it retained." | The tag, the variable or the alarm was deleted. |
| "Publishes the value and its quality in Sparkplug B whenever the value or the quality changes." | Sparkplug B: a tag or a variable, read only. |
| "Publishes the value and its quality in Sparkplug B whenever the value or the quality changes, and writes to the source each value a Sparkplug B command sends to its metric: each write is recorded in the journal with the broker's name." | Sparkplug B: a tag or a variable set to Read and write. |
| "Publishes whether the alarm is active and acknowledged, its state, priority and message in Sparkplug B whenever one of them changes." | Sparkplug B: an alarm. |
| "The source of this row no longer exists, so the edge node announces itself again without it." | Sparkplug B: the tag, the variable or the alarm was deleted. |

Under that line, in JSON, a row set to Read and write shows its **Write topic**, the row's topic followed by `/set`.
In Sparkplug B a second line names the metric the row is published as instead: of its Sparkplug B
device for a tag, of the station's edge node for a variable, and for an alarm the five metrics under
its name (Active, Acked, State, Priority and Message).

A change the list refuses is not stored: the reason is said in red above the row's fields, and
the change stays on the screen so you can correct it.

| The card says | When |
| --- | --- |
| "This source is already published to this broker: edit the row that publishes it." | Another row of this broker publishes the source. |
| "Another row of this broker already publishes on this topic: give this row a topic of its own." | Two rows of one broker would publish on one topic. |
| "Nothing can be published on a filter: the topic cannot hold + or #." | The topic holds a wildcard. |
| "A topic that starts with $ belongs to the broker: write another." | The topic starts with `$`. |
| "This topic cannot travel: it holds a character no topic may hold, or it is too long." | A control character, or more than an MQTT topic carries. |
| "A row that takes writes needs the tag or the variable they go to." | Read and write was chosen before a source. |
| "The source cannot be written: pick a tag with write access or a memory variable, or keep the row read only." | Read and write on an alarm, an expression variable, a read-only tag or a tag whose driver writes nothing. |
| "This topic and another row's topic on this broker differ only by /set, where a row takes writes: give this row another topic." | One row's topic is another row's topic followed by `/set`. |
| "This row of the list no longer exists. Reload and retry." | The row was deleted elsewhere while you edited it. |

What is published, and when, is on [Publishing](#publishing).

## Device {#device}

| Field | Meaning | Default |
| --- | --- | --- |
| **Broker · fixed** | The broker's name over its address, read only: a device is created under its broker and stays there. "The broker has no host yet" stands where the address will be. "Missing broker" means the device lost its broker and cannot be saved: "An MQTT broker is required." | the broker it is added under |
| **Protocol** | How the device publishes. **Plain MQTT**: "Each tag names the topic it reads and, for a JSON payload, the path to its value." **Sparkplug B**: one edge node, or one device behind it: "Each tag names a metric as the edge node's birth certificate declares it; its type comes with the metric." Changing it checks every tag again, since an address then reads as a topic or as a metric. | Plain MQTT |
| **Maximum time without a message (s)** | Plain MQTT only, 0 to 86400, 0 meaning never: "Tags stop counting as readings when no message arrives on their topic for this long. 0: never, for a topic published only when its value changes. Set it for a device that publishes at a steady rate, like a sensor that reports every few seconds." Outside that range the device is not saved: "The maximum time without a message must be from 0 to 86400 s." A Sparkplug B device has none: its death certificate already says when it went away. | 0 |
| **Group ID** | Sparkplug B only: the group the edge node publishes under, the level after `spBv1.0` in its topics. "A Sparkplug B device needs its group id." | empty |
| **Edge node ID** | Sparkplug B only: the edge node that publishes the metrics, as its NBIRTH names it. "A Sparkplug B device needs its edge node id." | empty |
| **Device ID** | Sparkplug B only: the device behind the edge node, as its DBIRTH names it. Empty, shown as "The edge node itself", reads the edge node's own metrics. | empty |

The device has no host, port, credentials or poll interval of its own: all of that is the
broker's. Each Sparkplug B ID is one topic level: "A Sparkplug B id may not contain '/', '+' or '#'."
A broker names one Sparkplug B edge node or device once, so a second device with the same
group, edge node and device IDs on that broker is refused: "Sparkplug B device Plant/Line1/Press1 on broker 'Plant' already belongs to device 'Press'."
Plain devices name nothing of their own on the broker, and two of them may read the same
topics.

The Base URI the header shows is the runtime address:
`mqtt://<broker id>?host=…&port=…&tls=0&keepAlive=60&clientId=…&mode=plain`, with
`&silence=…` when the device sets a maximum time without a message, and for a
Sparkplug B device `mode=sparkplug&group=…&node=…`, with `&device=…` behind an edge node.
It names the broker by its stable identity, never by its friendly name, and it never
carries the username or the password.

A copy of an MQTT device stays on the same broker. A Sparkplug B copy takes the first
identity no device on that broker has: its innermost ID, the device ID or, for the edge
node's own metrics, the edge node ID, is numbered the way the copy's name is
("Press1 (2)").

## Tag addressing {#tag-addressing}

The address field is **Topic** on a Plain MQTT device and **Metric** on a Sparkplug B one,
and it is required ("Topic is required for this driver."). A plain tag has three more
fields; a Sparkplug B metric has none, because it carries its own type and is written by
the protocol's own command.

| Field | Meaning | Default |
| --- | --- | --- |
| **Topic** | The one topic the tag reads, e.g. `plant/line1/temperature`. "A tag reads one topic, so its topic cannot hold the wildcards + or #." | required |
| **Value path** | Where the value sits inside a JSON payload: members separated by dots and array indexes in brackets, e.g. `sensors[0].value`. A member whose name holds a dot or a bracket is written in quotes inside brackets (`['flow.rate']`), and a leading `$.` is accepted. Empty, shown as "Whole payload", reads the whole payload as the value. A malformed path: "Write the value path as members separated by dots and array indexes in brackets, e.g. sensors[0].value." | empty |
| **Command topic** | Where a write is published. Empty keeps the tag read only: a tag whose Access writes needs one ("A writable tag needs the topic its writes are published on."), and it holds no wildcard ("Nothing can be published on a filter: the command topic cannot hold + or #."). Equipment seldom listens on the topic it reports on, which is why this is a field of its own. | empty |
| **Payload template** | What a write publishes: JSON with `{value}` once, outside quotes, where the written value goes, e.g. `{"setpoint": {value}}`. Empty, shown as "The value as text", publishes the value as text. "The payload template holds {value} exactly once, where the written value goes." / "Write the payload template as JSON, with {value} outside quotes where a JSON value goes." | empty |
| **Metric** | Sparkplug B: the metric name as the birth certificate declares it, e.g. `Inputs/Temperature`. | required |

The data types are Boolean, Int16, Int32, Int64, Float, Double, String and DateTime. The
read-only preview of the wire address is the topic followed by what applies of
`path`, `command` and `payload`
(`plant/line1/temperature?path=sensors[0].value&command=plant/line1/setpoint`), or the
metric name; a character that would end a part (`%`, `?`, `&`, `=`, `#`, `+`, a space)
is written as `%XX`.

## Reading {#reading}

The session keeps the last message of every topic it subscribed to, and a tag reads from
it:

| Situation | Plain MQTT tag | Sparkplug B tag |
| --- | --- | --- |
| The value | The last message on the topic: the whole payload as text, or the JSON value at the value path, converted to the tag's type. | The metric's value from the edge node's last birth or data message. |
| Its timestamp | When the message arrived. A message the publisher retained gives the first value as soon as the station subscribes. | The metric's own timestamp. |
| Nothing received yet | The tag waits: no value and no error, as with any driver before its first answer. A topic whose publisher does not retain has no value until it publishes again. With a **Maximum time without a message (s)**, the wait lasts that long from when the station began listening, and then the tag fails its read. | The same until the birth certificate arrives; the station asks the edge node to announce itself again. |
| The publisher gone quiet, the broker still up | With a **Maximum time without a message (s)**, a tag whose topic has had no message for longer fails its read, and its policy for [a device that did not answer](connector-value-pipeline#the-device-did-not-answer) decides what it shows; the next message brings it back. The time counts from the arrival of the last message, a retained one included. At 0 the tag keeps the last value it received. | Does not apply: the death certificate says it. |
| A message the tag cannot read | A failed read of that tag, with the reason: an empty message, a payload that is not JSON under a value path, nothing at the path, or a value the type cannot hold. | A failed read of that tag: a metric the birth certificate does not declare, or a value its type cannot hold. |
| The broker unreachable | Every device on the broker is disconnected, and its tags fail as any device's do. | The same. |
| A death certificate (NDEATH, DDEATH) | Does not apply. | The edge node and its devices, or that one device, read no value until they are born again; every other device on the broker goes on. |

A Boolean tag takes `true` or `false`, `1` or `0`, `on` or `off`. An integer tag takes a
fractional number rounded and refuses one outside its range. A DateTime tag takes ISO 8601
text or Unix milliseconds. A String tag takes the text as it is, and a JSON object or
array as its raw JSON. A Sparkplug B metric of an unsigned type reads into the next wider
signed one (UInt8 as Int16, UInt16 as Int32, UInt32 and UInt64 as Int64), so its whole
range reads.

## Writing {#writing}

A write goes through the same funnel as any other driver's: the write box takes the
engineering value, and the conversion stages are reversed before the raw value is
published (see [Connector](connector)).

| Tag | What is published | QoS | What the write reports |
| --- | --- | --- | --- |
| Plain MQTT | The value on the tag's **Command topic**, as text (`21.5`, `true`, the text itself, a date in ISO 8601), or put into the **Payload template** as a JSON value (a number as a number, a boolean as `true` or `false`, a text quoted and escaped). | 1, not retained | Done when the broker acknowledges it; refused, with the broker's reason, when an MQTT 5 broker turns it down; no response when no acknowledgment comes in time. |
| Sparkplug B | An NCMD (a metric of the edge node) or a DCMD (a metric of a device behind it) carrying the metric, under the alias its birth gave it and with the type its birth declared. | 0, not retained, as the Sparkplug B specification requires of commands | Done once sent. The edge node answers by publishing the new value, which is what the tag reads next. |

While the broker is unreachable, or the edge node or device has announced its death, a
write is refused as disconnected and nothing is sent. A Sparkplug B write to a metric the
birth certificate does not declare is refused, since the edge node would not take it, and
so is a value the metric's type cannot carry. A write is never retained: a retained
setpoint would be applied again by every subscriber that connects later.

## Publishing {#publishing}

With **Publication format** set to **JSON**, each row of a broker's [Publish card](#the-publish-card) is published on its topic, retained, at
QoS 1, through the connection the broker's devices use: the station never opens a second one to
a broker. A broker used only to publish needs no device. It stays under the MQTT driver, and its
state (Connecting…, Online, Unreachable) is the state of that connection. It publishes while it is
enabled and its list has rows; it has no switch of its own.

| Source | Message (JSON) |
| --- | --- |
| Tag | `{"value": 21.5, "quality": "good", "timestamp": "2026-10-04T12:30:15.250Z"}`: the engineering value the tag shows; `quality` is `good` for a reading that landed, `bad` with `value` null for one that did not, and `uncertain` for a replacement value, for the last value held, and for readings that stopped arriving (the value last published stays); the stamp of the reading, in UTC. |
| Logic variable | The same members: the variable's value and `good` while the variable is good, `null` and `bad` otherwise; the stamp of its last write or calculation. |
| Alarm | `{"active": true, "acked": false, "state": 1, "priority": "High", "message": "Oven above 80 °C", "timestamp": "2026-10-04T12:30:15.250Z"}`: whether it is active and acknowledged, `state` as a number (0 normal, 1 active and unacknowledged, 2 active and acknowledged, 3 cleared and unacknowledged), the priority by name (Critical, High, Medium, Low), the message (empty when none) and the stamp of its last transition, as the [OPC UA server](connector-opcua-served-nodes#where-a-client-finds-each-source) serves it. |

A value travels as its JSON type: a number, `true` or `false`, text, a date and time as ISO 8601
text; a number JSON cannot hold (NaN, an infinity) travels as `null`. A tag of a disabled device
publishes `null` and `bad`.

| Situation | What is published |
| --- | --- |
| The value or the quality changes | That row, checked five times a second. A tag read again at the same value is not published again: the stamp alone is no change. |
| The connection opens, or opens again | Every row, with its present state. |
| The broker is down | Nothing, and nothing waits in a queue: when the broker comes back, every row is published with its state at that moment. |
| The broker refuses a message, or does not acknowledge it | It is tried again after 30 seconds. The journal says so under the Connector area and the component `MQTT publication`, once and then in a summary every few minutes while it lasts. |
| A source is deleted | Nothing more; the broker keeps the last message it retained. |
| A row is deleted | Nothing more; its retained message stays on the topic until something else is published there. |

The station reads what it publishes and computes nothing for it: a tag's last reading, a
variable's value, an alarm's state. It subscribes to no topic of a read-only row, so a message another system publishes there never reaches the station; a row set to Read and write takes values on its write topic ([Writing back](#writing-back)). An agent reads the lists with
`mqtt_publish_list` and changes them with `mqtt_publish_upsert` and `mqtt_publish_delete`
([Tools](agent-tools#connector--configure)):

```text
mqtt_publish_upsert(brokerId: "<id from mqtt_broker_list>", source: "Connector.Simulated.Oven.Temperature")
mqtt_publish_upsert(brokerId: "<id from mqtt_broker_list>", source: "Alarms.Oven.HighTemperature", topic: "plant/oven/alarms/high")
mqtt_publish_upsert(id: "<id from mqtt_publish_list>", access: "ReadAndWrite")
mqtt_publish_list()
```

## Publishing in Sparkplug B {#publishing-in-sparkplug-b}

With **Publication format** set to **Sparkplug B**, the broker publishes the same list by the Sparkplug B
specification, with the station as an edge node: a Sparkplug B host, Ignition's MQTT Engine for one,
lists the station under its group and edge node, each device under it and every metric with its type,
without any mapping built by hand. The connection is the one the broker's devices use, as in JSON.

| Name | What it is |
| --- | --- |
| Group | **Group ID**, `GanterLab` while empty. |
| Edge node | The station's name, with each `/`, `+` and `#` made `_` (the three characters a Sparkplug B id cannot hold). |
| Device | Each Connector device with a row of one of its tags, named the same way. |
| A tag's metric | The tag's name, on its device. |
| A variable's metric | `Logic/<folders>/<variable>`, on the edge node. |
| An alarm's metrics | Five, on the edge node, under `Alarms/<folders>/<alarm>/`: `Active` and `Acked` (Boolean), `State` (a number, 0 normal, 1 active and unacknowledged, 2 active and acknowledged, 3 cleared and unacknowledged), `Priority` and `Message` (text). |

A `/` inside one name becomes `_`, so the folders are the only levels. Two devices, or two metrics,
whose names become the same take `_2`, `_3` in the order of their own names. The rows keep their topics,
which nothing is published on in Sparkplug B and which the list publishes on again when the broker goes
back to JSON.

| Message | When |
| --- | --- |
| NDEATH, the edge node's death | Handed to the broker as the connection's last will each time it opens, so the broker publishes it if the station drops off. The station also publishes it itself before it stops publishing: the broker set back to JSON, disabled or deleted, the list emptied, the group or the station's name changed, the runtime stopped. |
| NBIRTH, the edge node's birth | Each time the connection opens, when a host asks for a rebirth (`Node Control/Rebirth`), and when a variable or an alarm joins, leaves or is renamed: every metric of the edge node with its present value, with `bdSeq` and `Node Control/Rebirth`. |
| DBIRTH, a device's birth | After each NBIRTH, for every device, and for a device that joins the list: every tag of the device with its present value. |
| NDATA, DDATA | Five times a second, the metrics whose value or quality changed: one message for the edge node, one per device. |
| DDEATH, a device's death | When the last row of a device leaves the list, and before a device whose tags changed is born again. |

Every message goes out at QoS 0, never retained, except the NDEATH. The sequence number `seq` starts at
0 with each NBIRTH and counts every message after it up to 255, then starts again; `bdSeq` counts the
connections, so the NBIRTH and the NDEATH of one connection carry the same number. Nothing waits in a
queue while the broker is down.

| Type of the source | Sparkplug B type |
| --- | --- |
| `Boolean` | `Boolean` |
| `SByte`, `Byte` | `Int8`, `UInt8` |
| `Int16`, `UInt16` | `Int16`, `UInt16` |
| `Int32`, `UInt32` | `Int32`, `UInt32` |
| `Int64`, `UInt64` | `Int64`, `UInt64` |
| `Float`, `Double` | `Float`, `Double` |
| `String` | `String` |
| `DateTime` | `DateTime` |
| `Guid`, `ByteString` | `UUID`, `Bytes` |

A tag is published as its own type; a variable as the type the [OPC UA server](connector-opcua-served-nodes#where-a-client-finds-each-source) serves it as (a whole number as Int64, a real number as Double, a presentation value as text). Each metric carries its quality in the property `Quality`, the convention Sparkplug B hosts read: 192 good, 64 uncertain (with the value last published when the reading stopped arriving), 0 bad (the metric then has no value).

| Command | What the station does |
| --- | --- |
| NCMD `Node Control/Rebirth` | Publishes NBIRTH and every DBIRTH again, with the present values. |
| NCMD or DCMD naming the metric of a row set to Read and write | Writes the value to the source, as [Writing back](#writing-back) does: in the order commands arrive, recorded in the journal with the origin `MQTT <broker name>`. |
| Any other metric, a command without a value, or one the broker hands over retained | Nothing is written; the journal says so under the Connector area and the component `MQTT publication`. |

The station listens for commands to the edge node always, so a host can ask for a rebirth, and for
commands to a device only while one of its rows is set to Read and write. Switching a broker to Sparkplug B
makes its connection open again once, so the broker holds the edge node's last will; the broker's devices
ride through it as through any reconnection. The station is an edge node and not a primary host
application: it does not wait for a host's STATE message before it is born.

```text
mqtt_broker_upsert(id: "<id from mqtt_broker_list>", publishFormat: "SparkplugB", sparkplugGroupId: "Plant1")
mqtt_publish_list()
```

## Writing back {#writing-back}

A row of a [Publish card](#the-publish-card) set to **Read and write** takes values on its write
topic, the row's topic followed by `/set`: a cloud dashboard that reads `plant/oven/setpoint`
sends a new setpoint on `plant/oven/setpoint/set`. The station writes it the way a client of its
[Modbus server](connector-modbus-server) or its [OPC UA server](connector-opcua-served-nodes)
writes a row marked read and write.

| Situation | What happens |
| --- | --- |
| A row is set to Read and write | The station subscribes to its write topic, at QoS 1, on the connection the broker's devices use, and again each time that connection opens. A read-only row is not subscribed, so a message on its write topic never reaches the station. |
| A message arrives on the write topic | `{"value": 65}` or the bare value (`65`, `true`, `Recipe A`) is written to the source, in the order messages arrive: a tag the way an operator's write reaches it, a memory variable as it is. A JSON number, `true` or `false` and JSON text arrive as what they carry; anything that is not JSON is read as text. The value is converted to the source's type the way every write is, and a value the type cannot hold is refused. |
| The message carries no value | Nothing is written: an empty message, `null`, a JSON object without `value`, an array. |
| The broker hands over the message it retained | Nothing is written. A broker hands it over when the station subscribes, and a setpoint left there an hour ago is not a value somebody sends now. A value published while the station listens is written, retained or not. |
| The source no longer takes writes | Nothing is written: a tag set back to read only, a variable deleted. |
| The row is set back to Read only, moved to another topic or deleted, or the broker is disabled | The station stops listening on that write topic within a moment, and a message that arrives meanwhile writes nothing. |

The journal records each write with the origin `MQTT <broker name>` under the Connector area, as it
records an operator's write. A message that was not written is recorded under the Connector area
and the component `MQTT publication`, once and then in a summary every few minutes while it repeats.

The broker's username and password are the only guard: whoever the broker lets publish on a write
topic can change that source, and on a cloud broker shared with other people that is more than the
plant. Give the dashboard's account on the broker only the topics it needs. On a broker set to Sparkplug B
the same rows take Sparkplug B commands (NCMD, DCMD) instead of a write topic ([Publishing in Sparkplug B](#publishing-in-sparkplug-b)).

```text
"C:\Program Files\mosquitto\mosquitto_pub" -h localhost -u dashboard -P <password> -t plant/oven/setpoint/set -m 65
```

## Security {#security}

With **Use TLS**, the broker's certificate is accepted in one of two ways: its chain and host
name validate against the Windows certificate store, as any HTTPS connection of this computer
does, or you approved that very certificate under [Equipment access](#equipment-access) on
this driver's page. A broker whose certificate comes from a certificate authority of your own
is trusted once that authority is installed in the Windows store, and one with a self-signed
certificate needs nothing installed once its certificate is approved. Revocation is not
checked, and nothing accepts a certificate on sight: an unknown one is refused, the broker
reads Unreachable with the reason, and its devices wait for the approval. The password
travels inside the TLS connection; without TLS it travels in the clear, as the card warns.

## Equipment access {#equipment-access}

A broker with a self-signed certificate, or one issued by an authority of its own, presents a
certificate no root installed in Windows vouches for. That root does not need to be installed
on this computer. With **Use TLS**, the first connection is refused, the broker reads
Unreachable with the reason, its devices read **Waiting for certificate approval** in the tree
and on their pages, whose **Review certificate** button opens this card on the certificate,
and the certificate waits under **Equipment access** at the end of the MQTT driver's page, the
same card the OPC UA driver's page carries, until you decide:

| Group | What it lists | What you can do |
|---|---|---|
| Waiting for approval | Each refused certificate, under the name it claims: its SHA-256 fingerprint, the broker address it was met at, when it last tried and how many times, and its details (subject, issuer, domain names, validity dates). The one a device's Review certificate opened is marked Selected for review. | **Approve** accepts the certificate from the session's next connection attempt. **Block** keeps refusing it without listing it here again. **Discard all pending certificates** empties the list without approving or blocking anything. |
| Approved equipment | The certificates you approved or imported, with who decided and when, and the broker address each was last met at. | **Withdraw approval** closes the broker's session and returns its certificate to the list. **Import certificate** approves a public certificate file you already have (.der, .cer, .crt or .pem) before the first connection. |
| Blocked equipment | The certificates you blocked. A blocked certificate is refused even if Windows trusts it. | **Unblock** returns the certificate to the list, without approving it. |

Check the fingerprint with whoever manages the broker before you approve it. An approval
accepts that certificate and no other, and only within its validity dates; it stands in for
the root Windows lacks and for the name the certificate carries, so a broker reached by its IP
address connects although its certificate names another host, and nothing else about the
certificate is relaxed. When the broker answers an address whose certificate you approved with
another one, its devices stop reading, the new one waits here, and the station raises
"Certificate changed: `<name>`" with a **Review** button. A scan of the broker from the
Discover devices card meets the same decisions. A certificate Windows already trusts is
accepted as before and never appears here; nothing is approved automatically.

The broker's cause says the reason instead of a nameless TLS error: "The certificate of the
broker at `<host>:<port>` (the first 16 characters of the fingerprint) is waiting for your
approval under Equipment access on the MQTT driver's page. Reading starts once you approve
it." A blocked one reads "…is blocked at this station. Review it under Equipment access on the
MQTT driver's page." A certificate outside its validity dates, revoked or with a broken
signature is refused with that reason and is never listed, since no approval makes it sound.

The decisions are kept in the station's data folder, under `ua\pki\mqtt`, and survive a
restart. Only a role that may configure the Connector is drawn the verbs, and they act only
in the station's own window: a browser on another device reads the card without changing it,
and no agent tool approves a certificate (`connector_browse` reports the broker's devices with
`awaitingCertificate`). The list holds up to 100 certificates waiting for approval; once it
is full, a new one is still refused but not listed, and the card says so.

## Prepare the broker {#prepare-the-broker}

A broker has to accept connections from the station's computer, and to accept the station's
sign-in. Eclipse Mosquitto 2, the most common broker, does neither out of the box: with no
listener in its mosquitto.conf it listens only on its own computer, and once it has one it
refuses a client without a user name. MQTT does not say which broker answered, so the station
names Mosquitto as the common case, never as a certainty.

| The broker answers | What the broker's page says | What changes it |
| --- | --- | --- |
| Its computer refuses the port | The broker accepts connections only from its own computer | In mosquitto.conf, the line listener followed by the port (listener 1883), then a restart of the broker; the port allowed in its computer's firewall |
| It refuses a client without a user name | The broker does not accept a client without a user name | Username and Password on the Broker connection card, with an account of the broker; on Mosquitto 2 that account is made with mosquitto_passwd and named by password_file in mosquitto.conf, or anonymous clients are let in with allow_anonymous true |
| It refuses the user name or the password | The broker's own refusal, pointing to Username and Password on the Broker connection card | The account, on the broker or on the card |

The first two are said on the broker's page, in place of its English error, with these steps
and the port; its devices keep pointing up to the broker. The notice goes away by itself once
the session connects. The tree's mark and the journal keep the English cause, which ends with
the same Mosquitto steps.

## Installing a broker {#installing-a-broker}

The station needs a broker to connect to, and so does the equipment. Where you have none,
Eclipse Mosquitto is a small broker that runs as a Windows service:

1. Download the Windows installer from the downloads page of mosquitto.org and run it.
   It installs the service **Mosquitto Broker** in `C:\Program Files\mosquitto`.
2. Out of the box Mosquitto answers only on the computer it runs on. To let equipment on
   the network in, add a listener and a password file to `mosquitto.conf` in that folder:

   ```
   listener 1883
   password_file C:\Program Files\mosquitto\passwd
   ```

   Create the file with the account the station and the equipment will use (the command
   asks for the password):

   ```
   "C:\Program Files\mosquitto\mosquitto_passwd" -c "C:\Program Files\mosquitto\passwd" station
   ```

3. Restart the service from an administrator prompt, and allow TCP port 1883 in through
   Windows Defender Firewall for the equipment's network:

   ```
   net stop mosquitto
   net start mosquitto
   ```

4. On the broker's page, enter `localhost` as **Host** when the broker runs on the station's
   own computer, or that computer's address otherwise, **Port** 1883 and the account you
   created, then **Enable**.

To try a plain tag before the equipment is connected, publish a retained test value from
the same folder and point a tag's **Topic** at it:

```
"C:\Program Files\mosquitto\mosquitto_pub" -h localhost -u station -P <password> -t plant/line1/temperature -m 21.5 -r
```

## Limits worth knowing {#limits-worth-knowing}

- The MQTT driver is a client, not a broker: the equipment and the station connect to a
  broker, the station's own [MQTT broker](connector-mqtt-broker) or another one.
- A broker's Publish card goes out as JSON or as Sparkplug B, never both at once. In Sparkplug B
  the station is an edge node, not a primary host application, and uses no aliases, templates or
  historical values.
- The station is not a Sparkplug B host application: it publishes no STATE message. An
  edge node set to wait for a primary host before it publishes stays silent.
- Plain MQTT says nothing about a publisher that went quiet: a plain tag keeps the last
  value it received while the broker connection holds, dated by its arrival, unless its
  device sets a **Maximum time without a message (s)**. A topic published only when its
  value changes cannot tell a quiet publisher from a steady value, so its device keeps 0.
  Sparkplug B does say it, through the death certificate.
- The session starts clean: what was published while the station was not connected is
  not replayed, and a topic's retained message is what gives its tag a value again.
- A tag reads one topic, never a filter, and one value of it; a Sparkplug B metric that
  is an array, a data set, a template or a property set is not read.
- Only TCP, with or without TLS: no MQTT over WebSockets, no client certificate, and no
  broker older than MQTT 3.1.1.
- A device cannot be moved to another broker: create it under the other broker and delete
  the old one.
