Summary
The MTConnect.NET-AgentModule-MqttRelay module publishes the Agent Device's Availability state on {topicPrefix}/Probe/{agent-uuid}/Available — a four-segment topic under the Probe/# wildcard — both as the MQTT Last-Will-Testament (LWT) payload and as a retained "agent is up" message on connect. The payload is a raw unquoted string (AVAILABLE / UNAVAILABLE), not a JSON Probe envelope. This collides with the canonical topic contract that every topic under {prefix}/Probe/# carries a JSON envelope parseable by any MTConnect document consumer — subscribers using a Probe/# wildcard receive the non-JSON payload and fail to parse.
Environment
MTConnect.NET-Applications-Agents + MTConnect.NET-AgentModule-MqttRelay at v6.9.0 (official Docker image trakhound/mtconnect.net-agent:6.9.0, published 2025-10-16 — binary-equivalent to v6.9.0.2).
- Broker:
eclipse-mosquitto:2.0.22.
- Module configured with
topicStructure: Document (the default that encourages full-document publishes on Probe / Current / Sample / Asset).
- Comparison agent:
mtconnect/agent:latest = cppagent v2.7.0.7 — which publishes only three-segment topics under {prefix}/Probe/#.
Reproduction
Minimum rig — mqtt-relay module with topicStructure: Document:
# applications/MTConnect-Agent/appsettings.yaml
modules:
- mqtt-relay:
server: localhost
port: 1883
documentFormat: JSON-CPPAGENT-MQTT
topicPrefix: MTConnect/Document
topicStructure: Document
Run the agent, subscribe to the full Probe wildcard, and observe the four-segment topic:
mosquitto_sub -h localhost -t 'MTConnect/Document/Probe/#' -v
Observed — MT.NET
Two topic-patterns appear:
MTConnect/Document/Probe/<agent-uuid> { "Agent": { … } } ← canonical 3-segment Probe
MTConnect/Document/Probe/<device-uuid> { "Device": { … } } ← canonical 3-segment Probe
MTConnect/Document/Probe/<agent-uuid>/Available AVAILABLE ← NON-CANONICAL 4-segment, raw string
A consumer that decodes {prefix}/Probe/# as JSON envelopes gets a parse error on the fourth-segment topic (expected value at line 1 column 1 — classic serde-json "not JSON" failure).
Expected — cppagent v2.7.0.7 reference
cppagent running against the same device emits only three-segment Probe topics:
MTConnect/Probe/<agent-uuid> { "Agent": { … } }
MTConnect/Probe/<device-uuid> { "Device": { … } }
No /Available tail. cppagent places the agent's availability inside the JSON Probe envelope as a regular DataItem observation.
Evidence: sessions/tasks/h-feat-bridge/tmp/mtconnect-verify/ — live captures from both agents show the difference on any Probe-family subscription.
Authority
MT.NET source — verified root cause
agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs at master@HEAD (accessed 2026-04-21):
// line 738-744
private string GetAgentAvailableTopic()
{
if (Agent != null && _configuration != null)
{
return $"{_configuration.TopicPrefix}/{MTConnectMqttDocumentServer.ProbeTopic}/{Agent.Uuid}/Available";
}
}
MTConnectMqttDocumentServer.ProbeTopic is the string "Probe". So the composed topic is literally {prefix}/Probe/{agent-uuid}/Available.
The method is invoked in two places:
- LWT setup at line 120 (when the MQTT client is being configured):
clientOptionsBuilder.WithWillTopic(GetAgentAvailableTopic());
clientOptionsBuilder.WithWillPayload(System.Text.Encoding.UTF8.GetBytes(Availability.UNAVAILABLE.ToString()));
clientOptionsBuilder.WithWillQualityOfServiceLevel(MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);
clientOptionsBuilder.WithWillRetain(true);
The broker publishes this with the raw string UNAVAILABLE when the agent disconnects ungracefully.
- On-connect retained publish at line 417-422:
// Write Available (for Agent Device)
var availableTopic = GetAgentAvailableTopic();
var availablePayload = System.Text.Encoding.UTF8.GetBytes(Availability.AVAILABLE.ToString());
Published with the raw string AVAILABLE on agent startup.
In both cases the payload is the raw Availability enum's ToString() — never a JSON envelope.
Canonical topic contract
The {prefix}/Probe/# / {prefix}/Current/# / {prefix}/Sample/# / {prefix}/Asset/# families were introduced by cppagent's MQTT sink as carriers of JSON documents — one document per topic. cppagent's src/mtconnect/sink/mqtt_sink/mqtt_service.cpp composes topics as {prefix}/{kind}/{uuid} (three segments, always) and the payload is always a JSON envelope. MT.NET's mqtt-relay itself publishes three-segment topics for Probe / Current / Sample / Asset envelopes — only the Agent-Availability slice breaks the pattern by adding a fourth segment with a raw-string payload.
Every other document-family topic from mqtt-relay is JSON. The /Available exception is surprising; it collocates a raw-string channel with the document channel under a single wildcard prefix, where a consumer has no way to distinguish them by subscription pattern.
Impact
- Any consumer subscribing
MTConnect/Document/Probe/# and treating every received payload as a JSON envelope gets a parse error on the /Available fourth-segment topic. Error is silent from the subscriber's perspective (it's a decode failure on a legitimate-looking topic).
- Consumers that DO want the agent's availability have to add a special-case subscription to
{prefix}/Probe/{agent-uuid}/Available and a special-case decoder for the raw string. This is a MT.NET-specific burden not shared with cppagent-sourced streams.
- The LWT semantic (broker republishes
UNAVAILABLE if the agent disconnects ungracefully) is a legitimate feature; placing the LWT on a Probe sub-path is not.
Root cause localisation
Single method, single pattern, two call sites — both in agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs:
| Site |
Line |
What it does |
GetAgentAvailableTopic() |
738-744 |
Returns the non-canonical four-segment topic string |
LWT setup in Connect path |
~120 |
Calls WithWillTopic(GetAgentAvailableTopic()) + WithWillPayload("UNAVAILABLE") |
| On-connect publish |
~417-422 |
Creates MqttApplicationMessage with topic = GetAgentAvailableTopic() and raw string payload AVAILABLE |
Fix is one method's return value + verifying the downstream handling of the new topic.
Suggested fix
Move the agent-availability publication to a distinct topic namespace that does not overlap with the JSON-document Probe family:
-
Option A — dedicated Agents/ prefix (matches MT.NET's existing Agent-information slice, which already publishes on {prefix}/Agents/{uuid}/Information — see libraries/MTConnect.NET-MQTT/MTConnectMqttMessage.cs:74):
private string GetAgentAvailableTopic()
{
if (Agent != null && _configuration != null)
{
return $"{_configuration.TopicPrefix}/Agents/{Agent.Uuid}/Available";
}
}
Subscribers interested in agent availability watch {prefix}/Agents/#; subscribers interested in documents watch {prefix}/Probe/# / {prefix}/Current/# / etc. No wildcard collision.
-
Option B — JSON-wrap the payload:
Publish {"Availability": "AVAILABLE"} instead of raw AVAILABLE. Keeps the topic but makes the payload subscribable under a JSON-envelope wildcard. Cheaper change, but leaves the four-segment topology in place (still breaks naive Probe/# length-counting consumers).
Option A is preferred because it aligns with the existing {prefix}/Agents/* convention and eliminates the wildcard collision at the topic level.
-
Either option: update WithWillTopic(...) call site + on-connect publish call site + any documentation referencing the Probe-sub-topic pattern.
-
Regression test under MTConnect.NET-Tests/Mqtt/ asserting every topic under {prefix}/Probe/# is exactly three segments and the payload decodes as JSON.
Stability across MTConnect versions
Topic-layout conventions are outside the MTConnect XSDs (JSON-over-MQTT is not part of the normative standard — see the knowledge-base notes on json-format.md). The cppagent reference's canonical topology has been stable across cppagent v2 JSON lineage. The mqtt-relay module's four-segment outlier has been present since the module's introduction (based on a walk of the file's git history — the method is legacy code).
Related issues (filed in this batch)
References
- Source:
- cppagent reference topology:
src/mtconnect/sink/mqtt_sink/mqtt_service.cpp.
- Evidence captures (both agents, same broker, four-segment topic observed on MT.NET only):
sessions/tasks/h-feat-bridge/tmp/mtconnect-verify/.
Summary
The
MTConnect.NET-AgentModule-MqttRelaymodule publishes the Agent Device's Availability state on{topicPrefix}/Probe/{agent-uuid}/Available— a four-segment topic under theProbe/#wildcard — both as the MQTT Last-Will-Testament (LWT) payload and as a retained "agent is up" message on connect. The payload is a raw unquoted string (AVAILABLE/UNAVAILABLE), not a JSON Probe envelope. This collides with the canonical topic contract that every topic under{prefix}/Probe/#carries a JSON envelope parseable by any MTConnect document consumer — subscribers using aProbe/#wildcard receive the non-JSON payload and fail to parse.Environment
MTConnect.NET-Applications-Agents+MTConnect.NET-AgentModule-MqttRelayat v6.9.0 (official Docker imagetrakhound/mtconnect.net-agent:6.9.0, published 2025-10-16 — binary-equivalent to v6.9.0.2).eclipse-mosquitto:2.0.22.topicStructure: Document(the default that encourages full-document publishes on Probe / Current / Sample / Asset).mtconnect/agent:latest= cppagent v2.7.0.7 — which publishes only three-segment topics under{prefix}/Probe/#.Reproduction
Minimum rig —
mqtt-relaymodule withtopicStructure: Document:Run the agent, subscribe to the full Probe wildcard, and observe the four-segment topic:
mosquitto_sub -h localhost -t 'MTConnect/Document/Probe/#' -vObserved — MT.NET
Two topic-patterns appear:
A consumer that decodes
{prefix}/Probe/#as JSON envelopes gets a parse error on the fourth-segment topic (expected value at line 1 column 1— classic serde-json "not JSON" failure).Expected — cppagent v2.7.0.7 reference
cppagent running against the same device emits only three-segment Probe topics:
No
/Availabletail. cppagent places the agent's availability inside the JSON Probe envelope as a regular DataItem observation.Evidence:
sessions/tasks/h-feat-bridge/tmp/mtconnect-verify/— live captures from both agents show the difference on any Probe-family subscription.Authority
MT.NET source — verified root cause
agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.csatmaster@HEAD(accessed 2026-04-21):MTConnectMqttDocumentServer.ProbeTopicis the string"Probe". So the composed topic is literally{prefix}/Probe/{agent-uuid}/Available.The method is invoked in two places:
UNAVAILABLEwhen the agent disconnects ungracefully.AVAILABLEon agent startup.In both cases the payload is the raw
Availabilityenum'sToString()— never a JSON envelope.Canonical topic contract
The
{prefix}/Probe/#/{prefix}/Current/#/{prefix}/Sample/#/{prefix}/Asset/#families were introduced by cppagent's MQTT sink as carriers of JSON documents — one document per topic. cppagent'ssrc/mtconnect/sink/mqtt_sink/mqtt_service.cppcomposes topics as{prefix}/{kind}/{uuid}(three segments, always) and the payload is always a JSON envelope. MT.NET'smqtt-relayitself publishes three-segment topics forProbe/Current/Sample/Assetenvelopes — only the Agent-Availability slice breaks the pattern by adding a fourth segment with a raw-string payload.Every other document-family topic from
mqtt-relayis JSON. The/Availableexception is surprising; it collocates a raw-string channel with the document channel under a single wildcard prefix, where a consumer has no way to distinguish them by subscription pattern.Impact
MTConnect/Document/Probe/#and treating every received payload as a JSON envelope gets a parse error on the/Availablefourth-segment topic. Error is silent from the subscriber's perspective (it's a decode failure on a legitimate-looking topic).{prefix}/Probe/{agent-uuid}/Availableand a special-case decoder for the raw string. This is a MT.NET-specific burden not shared with cppagent-sourced streams.UNAVAILABLEif the agent disconnects ungracefully) is a legitimate feature; placing the LWT on a Probe sub-path is not.Root cause localisation
Single method, single pattern, two call sites — both in
agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cs:GetAgentAvailableTopic()ConnectpathWithWillTopic(GetAgentAvailableTopic())+WithWillPayload("UNAVAILABLE")MqttApplicationMessagewith topic =GetAgentAvailableTopic()and raw string payloadAVAILABLEFix is one method's return value + verifying the downstream handling of the new topic.
Suggested fix
Move the agent-availability publication to a distinct topic namespace that does not overlap with the JSON-document Probe family:
Option A — dedicated
Agents/prefix (matches MT.NET's existing Agent-information slice, which already publishes on{prefix}/Agents/{uuid}/Information— seelibraries/MTConnect.NET-MQTT/MTConnectMqttMessage.cs:74):Subscribers interested in agent availability watch
{prefix}/Agents/#; subscribers interested in documents watch{prefix}/Probe/#/{prefix}/Current/#/ etc. No wildcard collision.Option B — JSON-wrap the payload:
Publish
{"Availability": "AVAILABLE"}instead of rawAVAILABLE. Keeps the topic but makes the payload subscribable under a JSON-envelope wildcard. Cheaper change, but leaves the four-segment topology in place (still breaks naiveProbe/#length-counting consumers).Option A is preferred because it aligns with the existing
{prefix}/Agents/*convention and eliminates the wildcard collision at the topic level.Either option: update
WithWillTopic(...)call site +on-connect publishcall site + any documentation referencing the Probe-sub-topic pattern.Regression test under
MTConnect.NET-Tests/Mqtt/asserting every topic under{prefix}/Probe/#is exactly three segments and the payload decodes as JSON.Stability across MTConnect versions
Topic-layout conventions are outside the MTConnect XSDs (JSON-over-MQTT is not part of the normative standard — see the knowledge-base notes on
json-format.md). The cppagent reference's canonical topology has been stable across cppagent v2 JSON lineage. Themqtt-relaymodule's four-segment outlier has been present since the module's introduction (based on a walk of the file's git history — the method is legacy code).Related issues (filed in this batch)
Header.versionreports library assembly version instead of MTConnect release #127 —Header.versionreports library assembly version (adjacentmqtt-relaycorrectness concern).MTConnectStreams.schemaVersion/MTConnectDevices.schemaVersionhardcoded to"2.0"inJSON-cppagent-mqttformatter #128 —schemaVersionhardcoded"2.0"in the JSON formatter (adjacent).JSON-cppagent-mqtt: numeric Samplevalueemitted as JSON string instead of number token #129 — Numeric Sample value emitted as JSON string (adjacent).JSON-cppagent-mqtt:Header.schemaVersionabsent on MQTT Streams Header #130 —Header.schemaVersionabsent on MQTT Streams Header (adjacent).JSON-cppagent-mqtt:Header.testIndicatorabsent on MQTT Streams Header #131 —Header.testIndicatorabsent (adjacent).ASSET_COUNTDataItem not auto-rendered asrepresentation="DATA_SET"; stream observation namedAssetCountinstead ofAssetCountDataSet#132 —ASSET_COUNTnot auto-rendered asDATA_SET(adjacent).Organizers._systemslist is stale — causes asymmetric<Systems>auto-nesting inDevice.AddComponent()#134 —Organizers._systemslist is stale (orthogonal; separate code path).References
agent/Modules/MTConnect.NET-AgentModule-MqttRelay/Module.cslines 120 (LWT), 417-422 (on-connect publish), 738-744 (GetAgentAvailableTopic).libraries/MTConnect.NET-MQTT/MTConnectMqttRelay.cs— sibling full-document publisher for reference.libraries/MTConnect.NET-MQTT/MTConnectMqttDocumentServer.cslines 16-19 — canonical topic constants (ProbeTopic = "Probe").src/mtconnect/sink/mqtt_sink/mqtt_service.cpp.sessions/tasks/h-feat-bridge/tmp/mtconnect-verify/.