...
title:
...
"MQTT
...
Discovery Services — Connect, Browse, and Build Asset Navigation" tags: [mqtt, discovery-services, tagprovider, sparkplugb, asset-navigation,
...
dynamic-tags
...
, uns, iiot, datagrid, trendchart] description:
...
"Connect
...
FrameworX to
...
an
...
MQTT
...
broker
...
via
...
Discovery Services,
...
explore
...
its
...
namespace with the built-in MQTT browser,
...
link
...
data into
...
the
...
AssetTree,
...
and
...
build
...
a dynamic asset-navigation
...
UI
...
with
...
DataGrid,
...
TrendChart,
...
and
...
AlarmViewer
...
—
...
all
...
driven
...
by
...
Asset()
...
bindings."
...
version:
...
"1.0
...
"
...
author:
...
"Tatsoft"
...
...
This skill walks through a complete MQTT Discovery Services integration: connecting to a broker, exploring its topic tree, linking data into the UNS AssetTree, and building a dynamic asset-navigation UI where selecting a tree node changes all displayed data.
Prerequisite skill: Read skill-discovery-services first if you don't understand the difference between Device Module, DataLink, and Dynamic Tags.
Use when:
Do NOT use when:
open_solution or create_solution completed)Option A: Built-in broker for testing/demo
Navigate to Data Explorer → MQTT Tools and start the built-in services:
designer_action('navigate', 'DataExplorer.MQTTTools')
From MQTT Tools, start the built-in broker and simulator using the tab actions:
designer_action('builtin_broker', 'start')
designer_action('builtin_simulator', 'start')
This creates a local MQTT broker on localhost:1883 and a SparkplugB simulator publishing sample data (solar panels, cities, sensors).
Option B: External production broker
Ask the user for:
First, get the protocol schema to understand the PrimaryStation format:
get_table_schema('UnsTagProviders')
list_protocols('mqtt')
PrimaryStation format for MQTT — 14 semicolon-separated fields:
{Host};{Port};{ClientID};{Username};{Password};{CertFile};{KeyFile};{TLS};{CleanSession};{WillTopic};{QoS};{KeepAlive};{RetainPublish};{UseWebSocket};
Common configurations:
| Scenario | PrimaryStation |
|---|---|
| Local built-in broker | localhost;1883;FrameworX-001;;;;;None;False;;AtLeastOnce;10;False;False; |
| Remote broker | mqtt.factory.local;1883;FrameworX-001;;;;;None;False;;AtLeastOnce;10;False;False; |
| With authentication | broker.cloud.io;8883;MyClient;admin;password123;;;TLS 1.2;False;;AtLeastOnce;30;False;False; |
| WebSocket connection | broker.example.com;8083;MyClient;;;;;None;False;;AtLeastOnce;10;False;True; |
Write the connection:
{
"table_type": "...
UnsTagProviders",
"data": [...
{
...
"Name": "...
MQTT",
...
"...
Protocol": "...
MQTT",
...
"...
PrimaryStation": ...
"localhost;1883;FrameworX-001;;;;;None;False;;AtLeastOnce;10;False;False;",
...
...
"...
Separators": "...
BranchSeparator=/;AttributeSeparator=/;",
...
...
"...
Description": ...
"Production MQTT ...
Broker"
}]
}
For SparkplugB brokers, use MQTTspB protocol instead:
{
...
"Name": "SparkplugBroker",
"...
Protocol": "...
MQTTspB",
...
...
"...
PrimaryStation": "...
localhost;1883;FrameworX-SpB;;;;;None;False;;AtLeastOnce;10;False;False;",
...
...
"...
Separators": ...
"BranchSeparator=/;AttributeSeparator=/;",
"Description": "SparkplugB broker with ...
richer metadata"
}
Naming decision: The Name becomes the root of the dynamic namespace — all discovered topics appear under Tag.<Name>.*. Choose a meaningful name: ProductionMQTT, FactoryBroker, EdgeGateway, etc.
Separators: BranchSeparator=/;AttributeSeparator=/; maps MQTT topic hierarchy to UNS path hierarchy. This is almost always correct for MQTT.
Create an AssetTree folder that links to the Discovery Services connection. This makes the data navigable in the AssetsTree UI control.
get_table_schema('UnsAssetTree')
{
"table_type": "UnsAssetTree",
"data": [{
"Name": "Production/MQTTData",
"TagProviderLink": "MQTT",
"InitialBranch": "spBv1.0/GroupID",
"Description": "MQTT production data"
}]
}
Key fields:
From this folder down, the tree is dynamic — no further configuration needed.
The standard asset-navigation pattern uses a Layout with three regions: Header (top), Menu (left side with AssetTree), and Content (main area).
get_table_schema('DisplaysLayouts')
list_elements('Layout')
Read the existing startup layout first to understand current structure:
get_objects('DisplaysLayouts', detail='full', names=['Startup'])
Write or modify the layout:
{
"table_type": "DisplaysLayouts",
"data": [{
"Name": "Startup",
"Description": "Asset navigation — tree on left, dynamic content on right",
"Members": [
{"Region": "Header", "Docking": "Top", "Page": "Display.Header"},
{"Region": "Menu", "Docking": "Left", "...
Page": "...
Display.AssetNav"},
{"Region": "Content", "Page": "Display.MainPage"}...
]
...
}]
}
Note: Startup (layout ID 0) is the predefined startup layout — it loads on application launch. Read it first before modifying to preserve any existing configuration.
A simple Canvas display with the application title and optional navigation buttons:
get_table_schema('DisplaysList')
list_elements('TextBlock,IndustrialIcon')
{
"table_type": "DisplaysList",
"data": [{
"Type": "Display",
"Name": "Header",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Width": 1200,
"Height": 60,
"Description": "Application header bar",
"Elements": [
{
"Type": "TextBlock",
"Name": "Title",
"Left": 20,
"Top": 15,
"Text": "MQTT Production Monitor",
"FontSize": 18,
"FontWeight": "Bold"
}
]
}]
}
Use themed colors from list_elements('ThemeColors') instead of hardcoded hex values.
A Canvas display containing the AssetsTree control. When users click nodes, the platform updates Client.Context properties automatically.
list_elements('AssetsTree')
{
"table_type": "DisplaysList",
"data": [{
"Type": "Display",
"Name": "AssetNav",
"PanelType": "Canvas",
"DisplayMode": "Page",
"Width": 300,
"Height": 700,
"Description": "Asset tree navigation panel",
"Elements": [
{
"Type": "AssetsTree",
"Name": "NavTree",
"Left": 0,
"Top": 0,
"Width": 300,
"Height": 700
}
]
}]
}
What happens when the user clicks a node:
| Property | Description | Example value |
|---|---|---|
Client.Context.AssetPath | Full path of selected node | MQTT/spBv1.0/GroupID/Barcelona/Panel1 |
Client.Context.AssetName | Display name of selected node | Panel1 |
Client.Context.AssetNodeName | Leaf node name | Panel1 |
Client.Context.SelectedTag | Full tag path if a tag is selected | tag path string |
These properties drive the content display reactively — all elements bound to them update automatically.
The MainPage uses Asset() bindings and Client.Context to react to the user's tree selection. One display serves the entire asset hierarchy.
list_elements('DataGrid,TrendChart,TextBlock,AlarmViewer')
Dashboard layout approach:
{
"table_type": "DisplaysList",
"data": [{
"Type": "Display",
"Name": "MainPage",
"PanelType": "Dashboard",
"Columns": 2,
"Rows": 3,
"Description": "Dynamic content — reacts to AssetTree selection",
"Elements": [
{
"Type": "TextBlock",
"Name": "AssetTitle",
"Row": 0,
"Column": 0,
"ColumnSpan": 2,
"Text": "",
"LinkedValue": "@Client.Context.AssetName",
"FontSize": 20,
"FontWeight": "Bold"
},
{
"Type": "DataGrid",
"Name": "AssetData",
"Row": 1,
"Column": 0,
"ColumnSpan": 2,
"Height": 300
},
{
"Type": "TrendChart",
"Name": "AssetTrend",
"Row": 2,
"Column": 0,
"Height": 250,
"Duration": "00:05:00"
},
{
"Type": "AlarmViewer",
"Name": "AssetAlarms",
"Row": 2,
"Column": 1,
"Height": 250
}
]
}]
}
The key pattern — DataGrid bound to context:
The DataGrid displays all children (sub-tags and attributes) of whatever the user selected in the AssetTree. Check list_elements('DataGrid') for the exact property names to bind the DataGrid to Client.Context.AssetPath. The DataGrid automatically shows all tags below the selected path.
Asset() in CodeBehind (display events):
Public Sub DisplayOpening()
' Optionally set a default branch to expand on load
@Client.Context.TreeInitialBranch = "MQTT/spBv1.0/GroupID"
End Sub
designer_action('start_runtime')
After starting:
Client.Context propertiesTake a screenshot to verify:
get_screenshot(target='runtime')
| Aspect | MQTT (standard) | MQTTspB (SparkplugB) |
|---|---|---|
| Protocol name in FrameworX | MQTT | MQTTspB |
| Topic structure | Flat or custom hierarchy | `spBv1.0/GroupID/NBIRTH |
| Metadata | Topic names only | Rich: data types, birth/death certificates, metrics |
| Discovery quality | Basic (topic tree) | Full (typed variables with descriptions) |
| When to use | Generic MQTT brokers, custom topic schemes | Industrial IoT with SparkplugB-compliant edge nodes |
If the broker publishes SparkplugB messages, always prefer MQTTspB — you get typed variables and richer metadata.
Use FrameworX's built-in MQTT broker as a central data hub. External devices publish to it, and the Discovery Services connection reads from it — all on localhost.
{
"Name": "LocalHub",
"Protocol": "MQTT",
"PrimaryStation": "localhost;1883;FrameworX-Hub;;;;;None;False;;AtLeastOnce;10;False;False;",
"Separators": "BranchSeparator=/;AttributeSeparator=/;",
"Description": "Built-in broker as central data hub"
}
Start the built-in broker from Runtime Startup or via designer_action('builtin_broker', 'start').
Create multiple Discovery Services connections, each with a different Name:
{
"table_type": "UnsTagProviders",
"data": [
{"Name": "FactoryA", "Protocol": "MQTT", "PrimaryStation": "mqtt-a.factory.com;1883;..."},
{"Name": "FactoryB", "Protocol": "MQTT", "PrimaryStation": "mqtt-b.factory.com;1883;..."}
]
}
Create separate AssetTree folders for each. The same Layout and MainPage pattern works — Client.Context handles any source.
Use Discovery Services for browsing the full MQTT namespace, but create local tags with DataLink for the few points that need alarms and historian:
{
"table_type": "UnsTags",
"data": [
{
"Name": "Critical/ReactorTemp",
"DataType": "Float",
"DataLink": "/MQTT/plant/reactor/temperature",
"Min": 0,
"Max": 500,
"Description": "Reactor temperature — alarmed and historized"
}
]
}
Then configure alarms and historian on Critical/ReactorTemp as a normal tag. The DataLink binds it to the MQTT path automatically.
If connecting to a historian-capable Discovery Services connection (Canary, InfluxDB, etc.), TrendCharts using Asset() syntax automatically query historical data for the selected time range. Configure the Discovery Services connection with IsHistorian: true.
| Mistake | Why it happens | How to avoid |
|---|---|---|
| Creating local UnsTags for MQTT data | Habit from Device Module workflow | Dynamic tags don't need local tags. Only use DataLink for points needing alarms/historian. |
| Wrong PrimaryStation format | Forgetting semicolons or field order | Always call list_protocols('mqtt') first. The format is 14 semicolon-separated fields. |
| Topics with special chars not accessible | MQTT topics like spBv1.0/Group contain dots | Use quoted Asset() syntax: Asset("/ProviderName/spBv1.0/Group/Node") |
| AssetTree empty after runtime start | Connection not established or linked folder misconfigured | Check Services Monitor for connection status. Verify TagProviderLink matches the connection Name exactly. |
| Using MQTT protocol for SparkplugB broker | SparkplugB has specific message format | Use MQTTspB protocol for SparkplugB brokers — you get typed variables and metadata. |
| DataGrid shows nothing | Not bound to Client.Context properly | Check list_elements('DataGrid') for the correct binding properties. |
| Trying to set alarms on dynamic tags | No local tag exists for dynamic data | Create a local tag with DataLink for that specific path, then configure alarms on it. |
| Task | Tool call |
|---|---|
| Get MQTT protocol format | list_protocols('mqtt') |
| Get SparkplugB format | list_protocols('mqtt sparkplug') |
| Create Discovery Services connection | write_objects('UnsTagProviders', data=[...]) |
| Create AssetTree linked folder | write_objects('UnsAssetTree', data=[...]) |
| Browse AssetsTree element schema | list_elements('AssetsTree') |
| Browse DataGrid schema | list_elements('DataGrid') |
| Navigate to MQTT Tools | designer_action('navigate', 'DataExplorer.MQTTTools') |
| Start built-in broker | designer_action('builtin_broker', 'start') |
| Start built-in simulator | designer_action('builtin_simulator', 'start') |
| Check runtime state | get_state(target='runtime') |
| Navigate to Services Monitor | designer_action('navigate', 'ServicesMonitor') |