Skip to content

mqtt-relay module publishes Agent Availability on a non-canonical {prefix}/Probe/{agent-uuid}/Available four-segment topic #135

@ottobolyos

Description

@ottobolyos

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:

  1. 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.
  2. 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:

  1. 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.

  2. 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.

  3. Either option: update WithWillTopic(...) call site + on-connect publish call site + any documentation referencing the Probe-sub-topic pattern.

  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions