<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:discourse="http://discourse.org/rss/modules/discourse/"><channel><title>Rodrigo Cordeiro on DevBlog</title><link>https://devblog.criticalmanufacturing.com/author/rodrigo-cordeiro/</link><description>Recent content in Rodrigo Cordeiro on DevBlog</description><image><url>https://devblog.criticalmanufacturing.com/uploads/og.webp</url><link>https://devblog.criticalmanufacturing.com/uploads/og.webp</link></image><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>A template by [Heksagon](https://www.heksagon.net). Implemented for [Critical Manufacturing](https://www.criticalmanufacturing.com/).</copyright><lastBuildDate>Fri, 17 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://devblog.criticalmanufacturing.com/author/rodrigo-cordeiro/index.xml" rel="self" type="application/rss+xml"/><item><title>MQTT Simulator to UNS Journey</title><link>https://devblog.criticalmanufacturing.com/blog/20260418_mqttsimulatortounsjourney/</link><pubDate>Fri, 17 Apr 2026 00:00:00 +0000</pubDate><dc:creator>Rodrigo Cordeiro</dc:creator><guid>https://devblog.criticalmanufacturing.com/blog/20260418_mqttsimulatortounsjourney/</guid><description>A journey from raw MQTT equipment data to structured UNS topics, powered by MES context enrichment.</description><content:encoded><![CDATA[<p>In this blog post, we’ll use a simulator developed with the <a href="https://developer.criticalmanufacturing.com/explore/guides/customizations/automation/how-tos/test-orchestrator/getting-started/" target="_blank" rel="noopener">IoT Test Orchestrator</a> to feed a pipeline with equipment data collected via MQTT and transform that data into <a href="https://www.criticalmanufacturing.com/mes-for-industry-4-0/unified-namespace/" target="_blank" rel="noopener">UNS</a> (Unified Namespace) topics.</p>
<p>But wait… why do we need MES in this pipeline at all? Some might argue that tools like <a href="https://nodered.org/" target="_blank" rel="noopener">Node-RED</a> or <a href="https://n8n.io/" target="_blank" rel="noopener">n8n</a> are enough to build this kind of integration pipelines.</p>
<p>Suppose we want to generate Equipment State topics and publish them to MQTT as <strong>UNS</strong> topic.</p>
<p>The construction of <strong>UNS</strong> topics follows a set of rules. A <strong>UNS</strong> topic has a structure aligned with <strong>ISA‑95</strong>: Facility → Site → Area → Resource → Event. From there, it can be enriched with additional information relevant to the event being observed.</p>
<p>In simple scenarios like the one under analysis, the complementary information could be retrieved from an external source, such as a centralized equipment database maintained by an <strong>EAM - Enterprise Asset Management</strong> system for example.</p>
<p>But imagine the event you want to publish is a <strong>TrackIn</strong>. How do you enrich the raw data coming from the equipment? That contextual layer is precisely what MES provides. Pulling all this context into an external pipeline like Node‑RED is certainly possible — but at some point, it becomes counter‑productive.</p>
<p>A more straightforward and robust approach is to bring the flow of information into MES and let the Data Platform pipelines naturally enrich it:</p>
<ul>
<li>
<p>The equipment state‑model change along with other common shop-floor operations are core IoT Events in MES, and they already carry rich contextual information that can directly support <strong>UNS</strong> requirements and any additional needs.</p>
</li>
<li>
<p>For more advanced scenarios, MES can be extended — for example, sending an alert whenever a machine parameter is out of the control limits.</p>
</li>
</ul>
<p>So, let’s unfold this journey from MQTT to <strong>UNS</strong> topics in the next sections and take a few stops along the way to admire what we will earn in the voyage.</p>
<h2 id="introduction">Introduction</h2>
<p>We&rsquo;ll start by demoing how to design a simple simulator with behavior closer to real equipment, capable of generating MQTT topics. Then we&rsquo;ll define the ConnectIoT workflows to capture and observe how information naturally flows through MES while being enriched. Finally, we&rsquo;ll explore how MES out-of-the-box functionality generates the corresponding <strong>UNS</strong> topics. Of course, we&rsquo;ll also demonstrate extensibility through our Discord integration.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/Schema.png"
    alt="Schema"
    title="Schema"
  />
  <figcaption style="text-align: center">Schema</figcaption>
</figure>

</p>
<h2 id="mqtt-simulator">MQTT Simulator</h2>
<p>The same plugin used in the test orchestrator can be used to simulate our MQTT Broker in a simple .NET project. This can be easily achieved with a few steps.</p>
<p>Let&rsquo;s start from the beginning, step by step.</p>
<p>Create a directory to hold your project and initialize a .NET console application.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dotnet new console -n MQTTSimulator
</span></span></code></pre></div><p>This will create the scaffolding for your project.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/newconsoleapplication.png"
    alt="New console application"
    title="New Console Application"
  />
  <figcaption style="text-align: center">New Console Application</figcaption>
</figure>

</p>
<p>To fetch the MQTT plugin from CM repository, configure the <code>nuget.config</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#75715e">&lt;?xml version=&#34;1.0&#34; encoding=&#34;utf-8&#34;?&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;configuration&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;packageSources&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;add</span> <span style="color:#a6e22e">key=</span><span style="color:#e6db74">&#34;CMF&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;https://criticalmanufacturing.io/repository/nuget-hosted/index.json&#34;</span> <span style="color:#a6e22e">protocolVersion=</span><span style="color:#e6db74">&#34;3&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;add</span> <span style="color:#a6e22e">key=</span><span style="color:#e6db74">&#34;nuget.org&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;https://api.nuget.org/v3/index.json&#34;</span> <span style="color:#a6e22e">protocolVersion=</span><span style="color:#e6db74">&#34;3&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/packageSources&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;disabledPackageSources</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;solution&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;add</span> <span style="color:#a6e22e">key=</span><span style="color:#e6db74">&#34;disableSourceControlIntegration&#34;</span> <span style="color:#a6e22e">value=</span><span style="color:#e6db74">&#34;true&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/solution&gt;</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/configuration&gt;</span>
</span></span></code></pre></div><p>Copy all the libraries needed to our simulator to the libs directory:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/libs.png"
    alt="Libs"
    title="libs"
  />
  <figcaption style="text-align: center">libs</figcaption>
</figure>

</p>
<p>In the project <code>MQTTSimulator.csproj</code>, include all the packages we need for our simulator:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-xml" data-lang="xml"><span style="display:flex;"><span><span style="color:#f92672">&lt;Project</span> <span style="color:#a6e22e">Sdk=</span><span style="color:#e6db74">&#34;Microsoft.NET.Sdk&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;PropertyGroup&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;OutputType&gt;</span>Exe<span style="color:#f92672">&lt;/OutputType&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;TargetFramework&gt;</span>net9.0<span style="color:#f92672">&lt;/TargetFramework&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;ImplicitUsings&gt;</span>enable<span style="color:#f92672">&lt;/ImplicitUsings&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Nullable&gt;</span>enable<span style="color:#f92672">&lt;/Nullable&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/PropertyGroup&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;PackageReference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.ConnectIoT.TestOrchestrator.Core.Common&#34;</span> <span style="color:#a6e22e">Version=</span><span style="color:#e6db74">&#34;11.2.0-alpha24&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;PackageReference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.ConnectIoT.TestOrchestrator.Core.ScenarioBuilder&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Version=</span><span style="color:#e6db74">&#34;11.2.0-alpha09&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;PackageReference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.ConnectIoT.TestOrchestrator.Plugin.Simulator.MQTT&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Version=</span><span style="color:#e6db74">&#34;11.2.0-alpha08&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;PackageReference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.ConnectIoT.TestOrchestrator.Plugin.StartMode.Local&#34;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#a6e22e">Version=</span><span style="color:#e6db74">&#34;11.2.0-alpha10&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.LightBusinessObjects&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\Cmf.LightBusinessObjects.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.LoadBalancing&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\Cmf.LoadBalancing.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Cmf.MessageBus.Client&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\Cmf.MessageBus.Client.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Newtonsoft.Json&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\Newtonsoft.Json.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;SuperSocket.ClientEngine&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\SuperSocket.ClientEngine.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;System.Configuration.ConfigurationManager&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\System.Configuration.ConfigurationManager.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;System.Data.OleDb&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\System.Data.OleDb.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;System.Net.Http.Formatting&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\System.Net.Http.Formatting.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Reference</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;WebSocket4Net&#34;</span><span style="color:#f92672">&gt;</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">&lt;HintPath&gt;</span>Libs\WebSocket4Net.dll<span style="color:#f92672">&lt;/HintPath&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;/Reference&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">&lt;Folder</span> <span style="color:#a6e22e">Include=</span><span style="color:#e6db74">&#34;Objects\&#34;</span> <span style="color:#f92672">/&gt;</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&lt;/ItemGroup&gt;</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">&lt;/Project&gt;</span>
</span></span></code></pre></div><p>The first steps to design the Simulator are complete.</p>
<p>Next, the classe <code>ScenarioRunner.cs</code> will implement all the logic of our simulator:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span><span style="color:#66d9ef">using</span> IoTTestOrchestrator;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> MQTT;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> MQTT.Plugin;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> ScenarioBuilder.Implementations.Configuration;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Globalization;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Text;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Text.Json;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">using</span> System.Text.RegularExpressions;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> MQTTSimulator
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">ScenarioRunner</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> TestScenario? _scenario;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> ScenarioRunner()
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">public</span> <span style="color:#66d9ef">async</span> System.Threading.Tasks.Task RunAsync(<span style="color:#66d9ef">string</span>[]? args = <span style="color:#66d9ef">null</span>)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> scenario = <span style="color:#66d9ef">new</span> ScenarioConfiguration()
</span></span><span style="display:flex;"><span>                .WriteLogsTo(<span style="color:#e6db74">&#34;c:/temp/MQTT-Simulator.log&#34;</span>)
</span></span><span style="display:flex;"><span>                .ManagerId(<span style="color:#e6db74">&#34;MQTTManager&#34;</span>)
</span></span><span style="display:flex;"><span>                .ConfigPath(<span style="color:#e6db74">&#34;C:\\IOT\\AutomationManager\\AutomationManagerDemo\\config.full.json&#34;</span>)
</span></span><span style="display:flex;"><span>                .AddSimulatorPlugin&lt;MQTT.PluginMain&gt;(<span style="color:#66d9ef">new</span> SettingsBuilder()
</span></span><span style="display:flex;"><span>                    .Address(<span style="color:#e6db74">&#34;localhost&#34;</span>, <span style="color:#ae81ff">1883</span>)
</span></span><span style="display:flex;"><span>                    .StartBroker(<span style="color:#66d9ef">true</span>)
</span></span><span style="display:flex;"><span>                    .Build());
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            _scenario = <span style="color:#66d9ef">new</span> TestScenario(scenario);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">var</span> context = _scenario.Context();
</span></span><span style="display:flex;"><span>            MQTT.PluginMain? mqttSimulator = context.Simulators[<span style="color:#e6db74">&#34;MQTT&#34;</span>] <span style="color:#66d9ef">as</span> MQTT.PluginMain;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">using</span> var cts = <span style="color:#66d9ef">new</span> CancellationTokenSource();
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                _scenario.Start();
</span></span><span style="display:flex;"><span>                _scenario.StartSimulators();
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">&#34;[CNC] Waiting for MQTT broker...&#34;</span>);
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> WaitForMqttReadyAsync(mqttSimulator!, cts.Token);
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">&#34;[CNC] MQTT ready.&#34;</span>);
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">&#34;Press &#39;q&#39; to quit...&#34;</span>);
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">while</span> (!cts.IsCancellationRequested)
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">//Publish a simple topic</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">string</span> topic = <span style="color:#e6db74">&#34;test/topic&#34;</span>;
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">string</span> <span style="color:#66d9ef">value</span> = <span style="color:#e6db74">&#34;Hello, MQTT! &#34;</span> + DateTime.Now.ToString(<span style="color:#e6db74">&#34;O&#34;</span>, CultureInfo.InvariantCulture);
</span></span><span style="display:flex;"><span>                    mqttSimulator!.Publish(topic, <span style="color:#66d9ef">value</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">if</span> (Console.KeyAvailable)
</span></span><span style="display:flex;"><span>                    {
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">var</span> key = Console.ReadKey(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>                        <span style="color:#66d9ef">if</span> (key.KeyChar == <span style="color:#e6db74">&#39;q&#39;</span>)
</span></span><span style="display:flex;"><span>                        {
</span></span><span style="display:flex;"><span>                            cts.Cancel();
</span></span><span style="display:flex;"><span>                        }
</span></span><span style="display:flex;"><span>                    }
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">await</span> Task.Delay(<span style="color:#ae81ff">1000</span>, cts.Token);
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">catch</span> (Exception ex)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                Console.WriteLine(<span style="color:#e6db74">$&#34;[ERROR] {ex.Message}&#34;</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">finally</span>
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                _scenario.ShutdownSimulators();
</span></span><span style="display:flex;"><span>                _scenario.Shutdown();
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task WaitForMqttReadyAsync(PluginMain mqtt, CancellationToken ct)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Probe TCP port 1883 until the embedded broker is actually accepting connections,</span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// then allow a short stabilisation window for the MQTT client to authenticate.</span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">while</span> (!ct.IsCancellationRequested)
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">try</span>
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">using</span> var tcp = <span style="color:#66d9ef">new</span> System.Net.Sockets.TcpClient();
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">await</span> tcp.ConnectAsync(<span style="color:#e6db74">&#34;localhost&#34;</span>, <span style="color:#ae81ff">1883</span>, ct);
</span></span><span style="display:flex;"><span>                    <span style="color:#75715e">// Broker is listening — give the MQTT client time to finish its handshake.</span>
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">await</span> Task.Delay(<span style="color:#ae81ff">800</span>, ct);
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">return</span>;
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">catch</span> (OperationCanceledException) { <span style="color:#66d9ef">throw</span>; }
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">catch</span>
</span></span><span style="display:flex;"><span>                {
</span></span><span style="display:flex;"><span>                    <span style="color:#66d9ef">await</span> Task.Delay(<span style="color:#ae81ff">200</span>, ct);
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In <code>Program.cs</code> run the Scenario.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-cs" data-lang="cs"><span style="display:flex;"><span><span style="color:#66d9ef">namespace</span> MQTTSimulator
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">Program</span>
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">static</span> <span style="color:#66d9ef">async</span> Task Main(<span style="color:#66d9ef">string</span>[] args)
</span></span><span style="display:flex;"><span>        {
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">&#34;Starting Scenario Run&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">await</span> <span style="color:#66d9ef">new</span> ScenarioRunner().RunAsync(args);
</span></span><span style="display:flex;"><span>            Console.WriteLine(<span style="color:#e6db74">&#34;Finished Scenario Run&#34;</span>);
</span></span><span style="display:flex;"><span>            
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>There are a few things I would like to highlight.</p>
<p>The Simulator implements an <strong>MQTT Broker</strong> using the plugin provided by the Test Orchestrator. It needs the <code>config.full.json</code> file from the Automation Manager connected to MES and the address and port where the Broker will be published. Unlike what is typically needed in a test scenario, the Broker should keep running until it is no longer necessary, so I use a simple console polling loop that keeps the process alive and terminates only when the <code>Q</code> key is pressed.</p>
<p>Finally, build the solution:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dotnet build
</span></span></code></pre></div><p>The project should be something like this:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/dotnetbuild.png"
    alt="Project"
    title="Project"
  />
  <figcaption style="text-align: center">Project</figcaption>
</figure>

</p>
<p>To execute run the following command:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dotnet run
</span></span></code></pre></div><p>Using <a href="https://mqtt-explorer.com/" target="_blank" rel="noopener">MQTT Explorer</a>, we can connect to the MQTT Broker at <code>localhost</code> port <code>1883</code>.</p>
<hr>
<video class="video-shortcode" preload="auto" controls>
    <source src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/dotnetrun.mp4" type="video/mp4">
    There should have been a video here but your browser does not seem
    to support it.
</video>
<hr>
<p>This is our <strong>Minimum Viable Project (MVP)</strong>. But our challenge is to go further and implement additional functionality that brings us closer to the behavior of a real machine, like a CNC for example.</p>
<p>A vast amount of documentation exists on the internet explaining how a real CNC actually works, which you can reference. Or a simple prompt in your favorite LLM will do the trick too.</p>
<p>For our example, we’ve implemented the following functionalities in our CNC simulator:</p>
<ul>
<li>Loading a real <em>G‑code</em> file</li>
<li>Interpolating the tool path and moving the CNC head according to the provided G‑code file</li>
<li>Simulating the head temperature</li>
<li>Starting the simulation with <code>S</code></li>
<li>Triggering an anomaly by pressing the <code>A</code> key</li>
<li>Quitting the simulation with <code>Q</code></li>
</ul>
<hr>
<video class="video-shortcode" preload="auto" controls>
    <source src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/fullcodedotnetrun.mp4" type="video/mp4">
    There should have been a video here but your browser does not seem
    to support it.
</video>
<hr>
<h2 id="connectiot-workflows">ConnectIot Workflows</h2>
<p>With our equipment simulated and pushing topics to our MQTT Broker, let&rsquo;s continue our journey.</p>
<p>Now, we need to create a protocol that understands MQTT, with the address and port being the only things we need to configure:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/mqttprotocol.png"
    alt="MQTT Protocol"
    title="MQTT Protocol"
  />
  <figcaption style="text-align: center">MQTT Protocol</figcaption>
</figure>

</p>
<p>For the driver, 3 topics receive the values the equipment publish:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Topic</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>temperature</code></td>
<td><code>cnc/head/temperature</code></td>
</tr>
<tr>
<td><code>position</code></td>
<td><code>cnc/position</code></td>
</tr>
<tr>
<td><code>state</code></td>
<td><code>cnc/state</code></td>
</tr>
</tbody>
</table>
<p>Also, 3 events will listen to detect when each topic changes:</p>
<table>
<thead>
<tr>
<th>Event</th>
<th>Property</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>position_change</code></td>
<td><code>position</code></td>
</tr>
<tr>
<td><code>temperature_change</code></td>
<td><code>temperature</code></td>
</tr>
<tr>
<td><code>state_change</code></td>
<td><code>state</code></td>
</tr>
</tbody>
</table>
<p>The driver looks like this:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/mqttdriver.png"
    alt="MQTT Driver"
    title="MQTT Driver"
  />
  <figcaption style="text-align: center">MQTT Driver</figcaption>
</figure>

</p>
<p>The controller responsible for handling the pipeline starts, as usual, with the setup phase.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/workflowsetup.png"
    alt="MQTT Controller Setup"
    title="MQTT Controller Setup"
  />
  <figcaption style="text-align: center">MQTT Controller Setup</figcaption>
</figure>

</p>
<p>In our example, the <code>address</code> and <code>port</code> are already defined in the protocol itself, so there’s no need to configure them in the controller. In real‑world scenarios, however, it’s far more common to store all required parameters in a <a href="https://devblog.criticalmanufacturing.com/blog/20230413_iot_mes_as_configuration_owner/" target="_blank" rel="noopener">Configuration Entry</a> and apply them during the <code>OnSetup</code> phase.</p>
<h3 id="temperature-event">Temperature Event</h3>
<p>The first topic we will handle is <code>temperature</code>. For this type of data that is not directly related to any MES entities, MES provides the ability to collect data points using a special IoT Event called <strong>PostTelemetry</strong>, you can check all the APIs <a href="https://developer.criticalmanufacturing.com/11.3/reference/api-data-platform/index.html" target="_blank" rel="noopener">here</a>.</p>
<p>Under the Data Platform area in MES, you can find all IoT Events implemented by MES, including the option to create new ones:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/IoTEventDefinitions.png"
    alt="IoT Events"
    title="IoT Events"
  />
  <figcaption style="text-align: center">IoT Events</figcaption>
</figure>

</p>
<p>This <strong>PostTelemetry</strong> follows a standard format and is commonly used to ingest high‑cadence process parameters such as temperatures, vibration levels, speed, and other signals. These values are stored directly in the Data Platform for analytics, dashboards and to train Data Mining models for example.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/posttelemetry.png"
    alt="Post Telemetry"
    title="Post Telemetry"
  />
  <figcaption style="text-align: center">Post Telemetry</figcaption>
</figure>

</p>
<p>The workflow is triggered by the <code>temperature_change</code> event and publishes the data using <strong>PostTelemetry</strong>.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/workflowtemperature.png"
    alt="Workflow Temperature"
    title="Workflow Temperature"
  />
  <figcaption style="text-align: center">Workflow Temperature</figcaption>
</figure>

</p>
<p>If you look closely, you’ll notice that a few additional transformations and enrichment were applied. The resource information is passed along and the temperature topic arrives as a JSON payload that includes several fields not relevant to our use case.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;temperature&#34;</span>: <span style="color:#ae81ff">80.5</span>, <span style="color:#f92672">&#34;setpoint&#34;</span>:<span style="color:#ae81ff">80</span>, <span style="color:#f92672">&#34;state&#34;</span>:<span style="color:#e6db74">&#34;at_temperature&#34;</span>}
</span></span></code></pre></div><p>To extract the temperature value, I used the <strong>Get Object Property</strong></p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/convertertemperatura.png"
    alt="Converter Temperature"
    title="Converter Temperature"
  />
  <figcaption style="text-align: center">Converter Temperature</figcaption>
</figure>

</p>
<h3 id="status-event">Status Event</h3>
<p>The next ingestion we want to perform is the change of equipment status caused by an anomaly.</p>
<p>To simulate this event, pressing <code>A</code> in the simulator causes the equipment to gradually increase the temperature until it reaches the defined limit. This will trigger the Change of Status of the equipment and also a notification with a pop up, alerting that the machine has entered a fault state.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/workflowstatus.png"
    alt="Status workflow"
    title="Status workflow"
  />
  <figcaption style="text-align: center">Status workflow</figcaption>
</figure>

</p>
<p>The status topic value is a string, so no further transformation is needed.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/statustopic.png"
    alt="Status Topic"
    title="Status topic"
  />
  <figcaption style="text-align: center">Status topic</figcaption>
</figure>

</p>
<p>The status can assume also other values, depending on which phase the equipment is:</p>
<table>
<thead>
<tr>
<th>State</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>idle</code></td>
<td>Equipment is stopped</td>
</tr>
<tr>
<td><code>warming_up</code></td>
<td>Thermal stabilization in progress</td>
</tr>
<tr>
<td><code>running</code></td>
<td>Active production cycle</td>
</tr>
<tr>
<td><code>fault</code></td>
<td>Error condition detected</td>
</tr>
</tbody>
</table>
<p>Using the SEMI-E10 model the resource State will be adjusted accordingly:</p>
<table>
<thead>
<tr>
<th>State</th>
<th>SEMI-E10 State</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>idle</code></td>
<td>Standby</td>
</tr>
<tr>
<td><code>warming_up</code></td>
<td>Engineering</td>
</tr>
<tr>
<td><code>running</code></td>
<td>Productive</td>
</tr>
<tr>
<td><code>fault</code></td>
<td>Nonscheduled</td>
</tr>
</tbody>
</table>
<h3 id="position-event">Position Event</h3>
<p>For the <code>position_event</code>, the topic is also an object, making a <strong>Get Object Property</strong> converter necessary to extract the correct information.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{<span style="color:#f92672">&#34;x&#34;</span>:<span style="color:#ae81ff">0</span>,<span style="color:#f92672">&#34;y&#34;</span>:<span style="color:#ae81ff">0</span>,<span style="color:#f92672">&#34;z&#34;</span>:<span style="color:#ae81ff">5</span>,<span style="color:#f92672">&#34;feedrate&#34;</span>:<span style="color:#ae81ff">3000</span>,<span style="color:#f92672">&#34;mode&#34;</span>:<span style="color:#e6db74">&#34;home&#34;</span><span style="color:#960050;background-color:#1e0010">]</span>
</span></span></code></pre></div><p>For demonstration purposes, this information is only dumped to the log.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/workflowposition.png"
    alt="Position Workflow"
    title="Position Workflow"
  />
  <figcaption style="text-align: center">Position Workflow</figcaption>
</figure>

</p>
<h2 id="data-enrichment-and-visualization">Data Enrichment and Visualization</h2>
<p>After instantiating the Controller in our Automation Manager we can start the simulator and observe MES ingesting the data:</p>
<hr>
<video class="video-shortcode" preload="auto" controls>
    <source src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/allrunning.mp4" type="video/mp4">
    There should have been a video here but your browser does not seem
    to support it.
</video>
<hr>
<p>The head’s coordinates were logged (1), the <strong>PostTelemetry</strong> event was successfully published to the Data Platform (2) and the anomaly behaved exactly as expected: the trigger fired, the corresponding event was raised, and the equipment transitioned into a Non‑Scheduled state (3).</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/logcomplete.png"
    alt="ConnectIot Automation Manager Log"
    title="ConnectIot Automation Manager Log"
  />
  <figcaption style="text-align: center">ConnectIot Automation Manager Log</figcaption>
</figure>

</p>
<p>But there are also other interesting things happening under the hood.</p>
<p>Each data point published through <strong>PostTelemetry</strong> is stored in the Data Platform. By inspecting the <strong>PostTelemetry</strong> dataset, you can view these values in the Data Folder, already enriched with the contextual information provided by MES and explicitly implemented in the Status Workflow.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/PostTelemetryDataSet.png"
    alt="PostTelemetry Data"
    title="PostTelemetry Data"
  />
  <figcaption style="text-align: center">PostTelemetry Data</figcaption>
</figure>

</p>
<p>The status changes from the machine were also sent to the Data Platform data lake.</p>
<p>For each change in the Resource Status the following happens:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/cdmresourcestateflow.png"
    alt="Resource Change State flow"
    title="Resource Change State flow"
  />
  <figcaption style="text-align: center">Resource Change State flow</figcaption>
</figure>

</p>
<p>The CDM IoT Event <strong>ResourceStateChange</strong> is triggered:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/cdmresourcestate.png"
    alt="Resource Change State IoT Event"
    title="Resource Change State IoT Event"
  />
  <figcaption style="text-align: center">Resource Change State IoT Event</figcaption>
</figure>

</p>
<p>And a new record is saved to the correspondent CDM DataSet :</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/cdmresourcestatedataset.png"
    alt="Resource Change State DataSet"
    title="Resource Change State DataSet"
  />
  <figcaption style="text-align: center">Resource Change State DataSet</figcaption>
</figure>

</p>
<p>Both datasets can be explored in several ways: as Dashboards in Grafana, as reports in Stimulsoft, or consumed externally as an OData data source in tools like Power BI or Excel, or even used to train machine learning models.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/telemetrydashboard.png"
    alt="Grafana Telemetry Dashboard"
    title="Grafana Telemetry Dashboard"
  />
  <figcaption style="text-align: center">Grafana Telemetry Dashboard</figcaption>
</figure>

</p>
<h2 id="oob-uns-integration">OOB UNS Integration</h2>
<p>To generate the <strong>UNS</strong> topics, MES uses a special type of workflow designed specifically for the Data Platform. These workflows are triggered differently: instead of being invoked by a standard event, they listen to an IoT Event. When that event is raised, the workflow executes the corresponding logic within the context of the data that triggered it.</p>
<p>As previously explained, a Change Status operation raises the ResourceStateChange IoT Event. This means that if this IoT Event is configured as the trigger for a Data Platform workflow, the associated logic will be executed automatically.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/Sent2UNSController.png"
    alt="Sent to UNS Controller"
    title="Sent to UNS Controller"
  />
  <figcaption style="text-align: center">Sent to UNS Controller</figcaption>
</figure>

</p>
<p>What we need to construct the <strong>UNS</strong> topic is simply to extract the Resource from the context and then derive the remaining <strong>ISA‑95</strong> levels required for the topic structure.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/Sent2UNSControllerWorkflow.png"
    alt="Sent to UNS Controller"
    title="Sent to UNS Controller"
  />
  <figcaption style="text-align: center">Sent to UNS Controller</figcaption>
</figure>

</p>
<p>But you don’t need to worry about designing this workflow yourself — the transformation into a UNS topic is provided out of the box. With the Automation Controller window open the <strong>Cortex</strong> will guide design the right logic.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/BusinessScenario.png"
    alt="Business Scenarios"
    title="Business Scenarios"
  />
  <figcaption style="text-align: center">Business Scenarios</figcaption>
</figure>

</p>
<p>With all set the topic is published in the configured MQTT Broker with the <strong>ISA-95</strong> tree-structured, with the value enriched with all the context MES has associated with the event.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/Sent2UNSControllerMQTTExplorer.png"
    alt="Sent to UNS Controller"
    title="Sent to UNS Controller"
  />
  <figcaption style="text-align: center">Sent to UNS Controller</figcaption>
</figure>

</p>
<p>But you don’t need to worry about designing this workflow yourself — the transformation into a UNS topic is provided out of the box. With the Automation Controller window open, <a href="https://help.criticalmanufacturing.com/tutorials/modules/iot-data-platform/real-time-shopfloor-hierarchy/" target="_blank" rel="noopener">Business Scenario</a> guides you through the design.</p>
<hr>
<video class="video-shortcode" preload="auto" controls>
    <source src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/BusinessScenarioCortex.mp4" type="video/mp4">
    There should have been a video here but your browser does not seem
    to support it.
</video>
<hr>
<h2 id="customized-automation-task-libraries">Customized Automation Task Libraries</h2>
<p>One last destination is missing before we conclude our journey: the possibility to extend Connect IoT with complementary capabilities to enrich the pipeline even further. The perfect example is the ability to send notifications to <a href="https://discord.com/" target="_blank" rel="noopener">Discord</a>.</p>
<p>This kind of extensibility is possible using the Automation Task Libraries.</p>
<p>In any customization project, under the <code>/Cmf.Custom.IoT/Cmf.Custom.IoT.Packages</code> directory where the <strong>cmfpackage.json</strong> resides, run the command</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cmf new iot tasklibrary
</span></span></code></pre></div><p>A new package is created. Under the package directory run the command to create your task:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cmf new iot task 
</span></span></code></pre></div><p>At least an input parameter named <code>message</code> and two output parameters called <code>success</code> and <code>error</code> to provide feedback are necessary. Also a setting called <code>webhookUrl</code> to configure the webhook URL should be defined too.</p>
<p>After answering a few questions, the scaffold for the Task is created and ready to be customized:</p>
<p>The project tree should be something like this:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/mesatlmvp.png"
    alt="Project tree"
    title="Project tree"
  />
  <figcaption style="text-align: center">Project tree</figcaption>
</figure>

</p>
<p>All the magic happens in the method <code>OnChange</code> of our task, that will make a POST to the Web hook of your discord server.</p>
<p>To obtain the webhook, navigate to your Discord Server and under Settings/Integrations/Webhooks, you can copy the value:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/discordwebhook.png"
    alt="Discord Web Hook"
    title="Discord Web Hook"
  />
  <figcaption style="text-align: center">Discord Web Hook</figcaption>
</figure>

</p>
<p>The value should be pasted later in the Send to Discord Settings page:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/senddiscordsettings.png"
    alt="Send to Discord Task Settings"
    title="Send to Discord Task Settings"
  />
  <figcaption style="text-align: center">Send to Discord Task Settings</figcaption>
</figure>

</p>
<p>The complete code for the task is this:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-typescript" data-lang="typescript"><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">Task</span>, <span style="color:#a6e22e">System</span>, <span style="color:#a6e22e">TaskBase</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;@criticalmanufacturing/connect-iot-controller-engine&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">https</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;https&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> <span style="color:#f92672">*</span> <span style="color:#66d9ef">as</span> <span style="color:#a6e22e">http</span> <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;http&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">import</span> { <span style="color:#a6e22e">URL</span> } <span style="color:#66d9ef">from</span> <span style="color:#e6db74">&#34;url&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/** Default values for settings */</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">SETTINGS_DEFAULTS</span>: <span style="color:#66d9ef">SendDiscordMessageSettings</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>	<span style="color:#a6e22e">webhookUrl</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;&#34;</span>,
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/**
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * @whatItDoes
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * Sends a message to a Discord channel via a configured webhook URL.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * @howToUse
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * Configure the `webhookUrl` setting with your Discord webhook URL.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * Connect a string value to the `message` input and trigger `activate`.
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * ### Inputs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * * `any`    : **activate** - Activate the task
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * * `string` : **message**  - The message content to send to Discord
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * ### Outputs
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * * `bool`  : **success** - Triggered when the message is sent successfully
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * * `Error` : **error**   - Triggered when the task failed for some reason
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> *
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * ### Settings
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> * See {@see SendDiscordMessageSettings}
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"> */</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">@Task</span>.<span style="color:#a6e22e">Task</span>()
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">SendDiscordMessageTask</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">TaskBase</span> <span style="color:#66d9ef">implements</span> <span style="color:#a6e22e">SendDiscordMessageSettings</span> {
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** **Inputs** */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Message to send to the Discord channel */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** **Outputs** */</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Properties Settings */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Discord webhook URL */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">webhookUrl</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/**
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">     * When one or more input values is changed this will be triggered,
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">     * @param changes Task changes
</span></span></span><span style="display:flex;"><span><span style="color:#75715e">     */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">override</span> <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">onChanges</span>(<span style="color:#a6e22e">changes</span>: <span style="color:#66d9ef">Task.Changes</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">void</span>&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">changes</span>[<span style="color:#e6db74">&#34;activate&#34;</span>]) {
</span></span><span style="display:flex;"><span>            <span style="color:#75715e">// Reset activate to allow re-triggering without value change
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>            <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">activate</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">undefined</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_sendDiscordMessage</span>(<span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">webhookUrl</span>, <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">message</span>);
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_logger</span>.<span style="color:#a6e22e">info</span>(<span style="color:#e6db74">`SendDiscordMessage: Message sent successfully to Discord: </span><span style="color:#e6db74">${</span><span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">message</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">success</span>.<span style="color:#a6e22e">emit</span>(<span style="color:#66d9ef">true</span>);
</span></span><span style="display:flex;"><span>            } <span style="color:#66d9ef">catch</span> (<span style="color:#a6e22e">error</span>) {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">_logger</span>.<span style="color:#a6e22e">error</span>(<span style="color:#e6db74">`SendDiscordMessage: Failed to send message: </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">error</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>);
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">error</span>.<span style="color:#a6e22e">emit</span>(<span style="color:#a6e22e">error</span>);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Sends a POST request to the Discord webhook URL with the given message */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#a6e22e">_sendDiscordMessage</span>(<span style="color:#a6e22e">webhookUrl</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">message</span>: <span style="color:#66d9ef">string</span>)<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">void</span>&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Promise</span>((<span style="color:#a6e22e">resolve</span>, <span style="color:#a6e22e">reject</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">body</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">JSON</span>.<span style="color:#a6e22e">stringify</span>({ <span style="color:#a6e22e">content</span>: <span style="color:#66d9ef">message</span> });
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">parsedUrl</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">URL</span>(<span style="color:#a6e22e">webhookUrl</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">options</span> <span style="color:#f92672">=</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">hostname</span>: <span style="color:#66d9ef">parsedUrl.hostname</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">path</span>: <span style="color:#66d9ef">parsedUrl.pathname</span> <span style="color:#f92672">+</span> <span style="color:#a6e22e">parsedUrl</span>.<span style="color:#a6e22e">search</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">method</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;POST&#34;</span>,
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">headers</span><span style="color:#f92672">:</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;Content-Type&#34;</span><span style="color:#f92672">:</span> <span style="color:#e6db74">&#34;application/json&#34;</span>,
</span></span><span style="display:flex;"><span>                    <span style="color:#e6db74">&#34;Content-Length&#34;</span><span style="color:#f92672">:</span> <span style="color:#a6e22e">Buffer</span>.<span style="color:#a6e22e">byteLength</span>(<span style="color:#a6e22e">body</span>),
</span></span><span style="display:flex;"><span>                },
</span></span><span style="display:flex;"><span>            };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">transport</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">parsedUrl</span>.<span style="color:#a6e22e">protocol</span> <span style="color:#f92672">===</span> <span style="color:#e6db74">&#34;https:&#34;</span> <span style="color:#f92672">?</span> <span style="color:#a6e22e">https</span> : <span style="color:#66d9ef">http</span>;
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">req</span> <span style="color:#f92672">=</span> <span style="color:#a6e22e">transport</span>.<span style="color:#a6e22e">request</span>(<span style="color:#a6e22e">options</span>, (<span style="color:#a6e22e">res</span>) <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">&gt;=</span> <span style="color:#ae81ff">200</span> <span style="color:#f92672">&amp;&amp;</span> <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">statusCode</span> <span style="color:#f92672">&lt;</span> <span style="color:#ae81ff">300</span>) {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">resolve</span>();
</span></span><span style="display:flex;"><span>                } <span style="color:#66d9ef">else</span> {
</span></span><span style="display:flex;"><span>                    <span style="color:#a6e22e">reject</span>(<span style="color:#66d9ef">new</span> Error(<span style="color:#e6db74">`Discord API responded with status </span><span style="color:#e6db74">${</span><span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">statusCode</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>));
</span></span><span style="display:flex;"><span>                }
</span></span><span style="display:flex;"><span>                <span style="color:#a6e22e">res</span>.<span style="color:#a6e22e">resume</span>();
</span></span><span style="display:flex;"><span>            });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">on</span>(<span style="color:#e6db74">&#34;error&#34;</span>, <span style="color:#a6e22e">reject</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">write</span>(<span style="color:#a6e22e">body</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#a6e22e">req</span>.<span style="color:#a6e22e">end</span>();
</span></span><span style="display:flex;"><span>        });
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Right after settings are loaded, create the needed dynamic outputs. */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">override</span> <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">onBeforeInit</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">void</span>&gt; {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Initialize this task, register any event handler, etc */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">override</span> <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">onInit</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">void</span>&gt; {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">this</span>.<span style="color:#a6e22e">sanitizeSettings</span>(<span style="color:#a6e22e">SETTINGS_DEFAULTS</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Cleanup internal data, unregister any event handler, etc */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> <span style="color:#a6e22e">override</span> <span style="color:#66d9ef">async</span> <span style="color:#a6e22e">onDestroy</span>()<span style="color:#f92672">:</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">void</span>&gt; {
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">/** SendDiscordMessage Settings object */</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">export</span> <span style="color:#66d9ef">interface</span> <span style="color:#a6e22e">SendDiscordMessageSettings</span> <span style="color:#66d9ef">extends</span> <span style="color:#a6e22e">System</span>.<span style="color:#a6e22e">TaskDefaultSettings</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#75715e">/** Discord webhook URL */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">webhookUrl</span>: <span style="color:#66d9ef">string</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>To prepare the package to be imported to MES, run in the package directory the following commands:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#75715e"># Install dependencies</span>
</span></span><span style="display:flex;"><span>npm install
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Build</span>
</span></span><span style="display:flex;"><span>npm run build
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Pack into .tgz</span>
</span></span><span style="display:flex;"><span>npm pack
</span></span></code></pre></div><p>In MES navigate to the Automation Repository and add your new Package.</p>
<p>Finally in your workflow we can add our new task to send a Discord Message:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/worflowstatuswithdiscordsendmessage.png"
    alt="Send to Discord Task"
    title="Send to Discord Task"
  />
  <figcaption style="text-align: center">Send to Discord Task</figcaption>
</figure>

</p>
<p>Now MES also send a message to a Discord Server channel when the machine enter in fault.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260418_MQTTSimulatortoUNSJourney/discordserverchannel.png"
    alt="Send to Discord Channel"
    title="Send to Discord Channel"
  />
  <figcaption style="text-align: center">Send to Discord Channel</figcaption>
</figure>

</p>
<p>More details on how to implement customized tasks can be found in the documentation</p>
<p>For more detailed information on creating and implementing custom Automation Task Libraries, refer to the <a href="https://developer.criticalmanufacturing.com/11.3/explore/guides/customizations/automation/customization-components/customization_tasklibrary/" target="_blank" rel="noopener">Customization Task Library</a>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>As any passionate voyager will tell you, the best part of any journey isn’t the destination, but the path you take to get there. And we’ve seen just how much richer that path becomes when MES is on board. This journey, from raw MQTT data to MES enriched <strong>UNS</strong> topics highlights the true power of integrating equipment directly into MES. We’ve shown also how a simple simulator can approximate a near‑real data pipeline prepared to captures real‑time equipment data and enriches them with contextual information ready to be used in dashboards, reporting, and even model training.</p>
<h2 id="author">Author</h2>
<p>Hello, my name is Rodrigo Cordeiro. I’m passionate about machine integration, automation and transforming industrial data into clear, useful, decision‑ready insights.</p>
<p>You can reach me on <a href="https://www.linkedin.com/in/rodrigocordeiro/" target="_blank" rel="noopener">LinkedIn</a></p>
<p>Skills: ConnectIoT | Data Analyst | Data Scientist</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/contributors/blogger_rodrigo_cordeiro.jpg"
    alt="Rodrigo Cordeiro"
    title="Connect IoT Engineer"
  />
  <figcaption style="text-align: center">Connect IoT Engineer</figcaption>
</figure>

</p>
]]></content:encoded></item><item><title>Did you know that Critical Manufacturing MES can "make a coffee"?</title><link>https://devblog.criticalmanufacturing.com/blog/20260413_connectiot_didyouknowmescantakeacoffee/</link><pubDate>Mon, 13 Apr 2026 00:00:00 +0000</pubDate><dc:creator>Rodrigo Cordeiro</dc:creator><guid>https://devblog.criticalmanufacturing.com/blog/20260413_connectiot_didyouknowmescantakeacoffee/</guid><description>Integrate equipment with BLO Protocol</description><content:encoded><![CDATA[<p>Conceptually, the process of making a coffee can be modeled just like any other shop‑floor operation. You could have the material consumptions, a report production and even equipment availability. In practice, it behaves exactly like a standard manufacturing process scaled down to something as familiar as preparing a cup of coffee.</p>
<p>If the coffee machine has IoT capabilities, you can go even further. The entire lifecycle of the coffee cup can be fully integrated into Critical Manufacturing MES, from material consumption to machine events, creating a complete digital thread for your morning espresso.</p>
<p>A recent international industrial exposition (Hannover Messe), where CM will participate with a booth, provided the perfect opportunity to revisit this project, upgrade it to Version 11 of MES and demonstrate how far this simple idea can go.</p>
<h2 id="the-original-sin">The original sin</h2>
<p>Let me start from the beginning. A few years ago the ConnectIoT team was challenged to integrate a Nespresso machine with MES in version 6. At that time, the only information available was that the equipment communicated via the BLE protocol and through the official mobile application, the machine could be instructed to brew coffee and several relevant information could be retrieved too.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/Nespresso.png"
    alt="Nespresso"
    title="Nespresso Prodigio model"
  />
  <figcaption style="text-align: center">Nespresso Prodigio model</figcaption>
</figure>

</p>
<p>After extensive investigation, using not always “official” tools and methods — we were able to extract all the necessary information to enable full integration.</p>
<p>For the specific Nespresso model used in this Proof of Concept, the following parameters are required to configure the BLE communication protocol:</p>
<table>
<thead>
<tr>
<th>Property</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>Mac Address</td>
<td>F8:BA:11:2E:9F:70</td>
</tr>
<tr>
<td>BLE Device ID</td>
<td>f8ba112e9f70</td>
</tr>
<tr>
<td>BLE Device Name</td>
<td>Prodigio_F8BA112E9F70</td>
</tr>
</tbody>
</table>
<p>In the BLE protocol, data points of the equipment are exposed as <code>BLE Characteristics</code>. The Nespresso machine is no different:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Characteristic UUID</th>
<th>Type</th>
<th>Writable</th>
<th>Readable</th>
</tr>
</thead>
<tbody>
<tr>
<td>TxLevel</td>
<td>TX Level to start communication</td>
<td>06aa3a61f22a11e39daa0002a5d5c51b</td>
<td>String</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>ClubIdMember</td>
<td>Code to authorize communication</td>
<td>06aa3a41f22a11e39daa0002a5d5c51b</td>
<td>String</td>
<td>True</td>
<td>False</td>
</tr>
<tr>
<td>MachineSpecificParameters</td>
<td>State of top slider</td>
<td>06aa3a22f22a11e39daa0002a5d5c51b</td>
<td>Integer</td>
<td>False</td>
<td>True</td>
</tr>
<tr>
<td>MachineStatus</td>
<td>Status of the Machine</td>
<td>06aa3a12f22a11e39daa0002a5d5c51b</td>
<td>String</td>
<td>False</td>
<td>True</td>
</tr>
<tr>
<td>CapsuleStock</td>
<td>Number of capsules stored</td>
<td>06aa3a15f22a11e39daa0002a5d5c51b</td>
<td>Integer</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>Command</td>
<td>Command to brew coffee</td>
<td>06aa3a42f22a11e39daa0002a5d5c51b</td>
<td>String</td>
<td>True</td>
<td>True</td>
</tr>
<tr>
<td>CommandResponse</td>
<td>Command response</td>
<td>06aa3a52f22a11e39daa0002a5d5c51b</td>
<td>String</td>
<td>False</td>
<td>True</td>
</tr>
</tbody>
</table>
<p>Likely in other protocol, the <code>Events</code> are a key element in the communication with the equipment. An <code>Event</code> is triggered when something changes in the machine, in our case we need:</p>
<table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>BLE Characteristic</th>
</tr>
</thead>
<tbody>
<tr>
<td>OnSliderStateChange</td>
<td>Triggered when the Slider is manually handled</td>
<td>MachineSpecificParameters</td>
</tr>
<tr>
<td>OnMachineStatusChange</td>
<td>Triggered when the machine status changes (water level, state, etc.)</td>
<td>MachineStatus</td>
</tr>
<tr>
<td>OnCapsuleStockChange</td>
<td>Triggered when a capsule drops inside the machine after processing</td>
<td>CapsuleStock</td>
</tr>
<tr>
<td>OnCommandResponseChange</td>
<td>Triggered when a coffee brew command is invoked with the reply</td>
<td>CommandResponse</td>
</tr>
</tbody>
</table>
<p>In MES, the driver used to integrate the Nespresso machine looked like this:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/NespressoDriver.png"
    alt="NespressoDriver"
    title="Nespresso driver"
  />
  <figcaption style="text-align: center">Nespresso driver</figcaption>
</figure>

</p>
<p>If you want a deeper understanding of how the Nespresso machine works and the reverse‑engineering techniques involved, consult the documentation below. These resources provide additional insights and practical examples how similar process were carried out:</p>
<ul>
<li><a href="https://github.com/brandtoliver/brew-api" target="_blank" rel="noopener">https://github.com/brandtoliver/brew-api</a></li>
<li><a href="https://gist.github.com/farminf/94f681eaca2760212f457ac59da99f23" target="_blank" rel="noopener">https://gist.github.com/farminf/94f681eaca2760212f457ac59da99f23</a></li>
</ul>
<h2 id="modelling">Modelling</h2>
<p>The modelling itself can be as simple as <code>brew me a coffee with a button click</code> or as sophisticated and complex as you want. A more robust modelling could allow the measuring of water and energy in each of the brews using for example BOMs and Consumable Feeds. Nevertheless, by default MES gives you the daily production of coffee by type with just the TrackIn/Trackout logic and the timeline of production/standby from the resource state, or even the OEE. But let’s not overcomplicate things.</p>
<p>The intention is to demonstrate how this type of integration can be achieved, focusing primarily on the ConnectIoT perspective. Once the communication layer is in place, the possibilities for building visually appealing dashboards, feature‑rich functionalities or technically challenging scenarios are virtually endless.</p>
<blockquote>
<p>A flow with two steps allows the Nespresso to be fully integrated in the MES logic. A set of Coffee Materials, based in the CoffeeCapsule Product, is created in the step Nespresso with state Queued whenever a new box of capsules is put at the disposal of the machine.
When the machine starts brewing the material coffee is <strong>Dispatched and TrackedIn</strong> and stays in process till the machine informs MES that the brew is finished. Considering the brew selected the Material Type changes to (Lungo, Espresso or Ristretto). Then a Trackout and MoveNext sends the material to the next step - <em>The Trash</em>.
Additionally, the machine informs us of the water consumed during the process and, considering the quantity last refilled, allows us to calculate the Water Level by taking into consideration what each brew level takes and the full tank capacity (750ml):</p>
<ul>
<li>Lungo - 110ml</li>
<li>Espresso - 40ml</li>
<li>Ristretto - 25ml</li>
</ul>
</blockquote>
<h2 id="controller-workflows">Controller Workflows</h2>
<h3 id="setup-and-initial-configurations">Setup and initial configurations</h3>
<p>Before communication is enabled and fully bidirectional, the Bluetooth device <strong>Prodigio_F8BA112E9F70</strong> must be paired with the computer.</p>
<p>Two BLE characteristics must be assigned specific values for MES to be allowed to read from and write to the equipment:</p>
<ul>
<li>TxLevel = 1</li>
<li>ClubIdMember = 88022c152c030494</li>
</ul>
<p>This configuration can be easily applied in the Controller Setup, during the <code>OnSetup</code> phase:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/Setup.png"
    alt="Setup"
    title="Setup workdflow"
  />
  <figcaption style="text-align: center">Setup workdflow</figcaption>
</figure>

</p>
<h3 id="semi-e10-state">SEMI E10 State</h3>
<p>Manipulating the Slider in the machine, the Event <code>OnSliderStateChange</code> changes the Resource state, based on the SEMI E10 model of the equipment:</p>
<ul>
<li>Slide Open -  Resource goes to Standby</li>
<li>Slide Close-  Resource goes to Productive</li>
</ul>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/NespressoButtons.png"
    alt="Nespresso Buttons"
    title="Nespresso Buttons"
  />
  <figcaption style="text-align: center">Nespresso Buttons</figcaption>
</figure>

</p>
<p>The <code>MachineSpecificParameters</code> property of the event, assumes 3 distinct values:</p>
<table>
<thead>
<tr>
<th>Value</th>
<th>Slide</th>
</tr>
</thead>
<tbody>
<tr>
<td>0, 10</td>
<td>Open</td>
</tr>
<tr>
<td>2</td>
<td>Close</td>
</tr>
</tbody>
</table>
<p>Some buffer operations are needed to implement this logic, namely:</p>
<ul>
<li>Applying a Logic AND 2</li>
<li>Shift Right of 1</li>
<li>Convert to Unsigned Integer</li>
</ul>
<table>
<thead>
<tr>
<th>Value</th>
<th>State</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Equipment State to Standby</td>
</tr>
<tr>
<td>1</td>
<td>Equipment State to Productive</td>
</tr>
</tbody>
</table>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/SlideChange.png"
    alt="SlideChange"
    title="Slide Change workflow"
  />
  <figcaption style="text-align: center">Slide Change workflow</figcaption>
</figure>

</p>
<h3 id="capsule-handling">Capsule Handling</h3>
<p>The equipment is capable of reporting the number of capsules consumed and the <code>CapsuleStock</code> event is triggered whenever this value changes. However, this functionality was not implemented in this Proof of Concept. Instead, capsule stock is managed directly in MES, which provides a more robust and reliable approach.</p>
<h3 id="coffee-management">Coffee Management</h3>
<p>To brew a coffee, some logic on the MES side triggers the Action Group <code>brew</code>. This can be achieved using a DEE that will be executed in a button click for example with the CoffeeType as input. To trigger each of the following Coffee types, the corresponding code should be written in the BLE Characteristic of the Nespresso equipment:</p>
<table>
<thead>
<tr>
<th>Type</th>
<th>Command Code</th>
</tr>
</thead>
<tbody>
<tr>
<td>Lungo</td>
<td>03050704000000000002</td>
</tr>
<tr>
<td>Espresso</td>
<td>03050704000000000001</td>
</tr>
<tr>
<td>Ristretto</td>
<td>03050704000000000000</td>
</tr>
</tbody>
</table>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/CoffeeManagment.png"
    alt="CoffeeManagment"
    title="Coffee Managment Workflow"
  />
  <figcaption style="text-align: center">Coffee Managment Workflow</figcaption>
</figure>

</p>
<p>The response will trigger the <code>OnCommandResponseChange</code> event. The <code>CommandResponse</code> Event Property gives the status in buffer position <code>(3,1)</code>:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/CommandResultResponse.png"
    alt="Command ResultResponse"
    title="Command Result Response"
  />
  <figcaption style="text-align: center">Command Result Response</figcaption>
</figure>

</p>
<table>
<thead>
<tr>
<th>CommandResult</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr>
<td>32</td>
<td>Ok</td>
</tr>
<tr>
<td>22</td>
<td>Not Ok</td>
</tr>
<tr>
<td>36</td>
<td>Error</td>
</tr>
</tbody>
</table>
<p>In case of error, the bit <code>(4,1)</code> could be:</p>
<table>
<thead>
<tr>
<th>Bit Value</th>
<th>Error Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Invalid State</td>
</tr>
<tr>
<td>8</td>
<td>Slide is Open</td>
</tr>
<tr>
<td>18</td>
<td>Need to Insert a new Coffee capsule</td>
</tr>
</tbody>
</table>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/CoffeeManagmentCommandResponse.png"
    alt="CoffeeManagment Command Response"
    title="CoffeeManagment Command Response"
  />
  <figcaption style="text-align: center">CoffeeManagment Command Response</figcaption>
</figure>

</p>
<p>A notification is created with popup of the error.</p>
<h3 id="event-handling">Event Handling</h3>
<p>During the brew process the <code>OnMachineStatusChange</code> event is triggered, giving the status of the equipment:</p>
<p>The <code>OnMachineStatusChange</code> event exposes the machine state through buffer positions that indicate engine activity and water consumption levels.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/StatusBuffer.png"
    alt="StatusBuffer"
    title="Status Buffer"
  />
  <figcaption style="text-align: center">Status Buffer</figcaption>
</figure>

</p>
<table>
<thead>
<tr>
<th>Buffer Position</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>Bit 1</td>
<td>Engine Status</td>
</tr>
<tr>
<td>Bits 6 and 7</td>
<td>Water Level</td>
</tr>
</tbody>
</table>
<h3 id="engine-state">Engine State</h3>
<p>The buffer <code>bits(1,1)</code> give you the state of the engine during the brew:</p>
<table>
<thead>
<tr>
<th>Engine State</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>2</td>
<td>Idle</td>
</tr>
<tr>
<td>132</td>
<td>Starting/Stopping</td>
</tr>
<tr>
<td>130</td>
<td>Brewing coffee</td>
</tr>
</tbody>
</table>
<p>The Status commutes following this pattern:</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/EngineStatusChangeDuringBrew.png"
    alt="EngineStatusChangeDuringBrew"
    title="Engine Status Change During Brew"
  />
  <figcaption style="text-align: center">Engine Status Change During Brew</figcaption>
</figure>

</p>
<p><strong>State Transitions:</strong></p>
<ul>
<li><code>2 → 130</code>: Idle to Starting</li>
<li><code>130 → 132</code>: Starting to Brewing</li>
<li><code>132 → ...... → 132</code>: Brewing continues</li>
<li><code>132 → 130</code>: Brewing to Stopping</li>
<li><code>130 → 2</code>: Stopping to Idle</li>
</ul>
<p>The Material TrackIn and TrackOut are done in the following transitions:</p>
<ul>
<li><code>130 → 132</code>: <strong>TrackIn</strong> of material</li>
<li><code>132 → 130</code> or <code>132 → 2</code>: <strong>TrackOut</strong> of in-process material</li>
</ul>
<p>Notifications in case of error are sent, as well as a notification of successful brewing.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/EventHandling.png"
    alt="EventHandling"
    title="Event Handling workflow"
  />
  <figcaption style="text-align: center">Event Handling workflow</figcaption>
</figure>

</p>
<h3 id="water-level">Water Level</h3>
<p>Bits 6 and 7 provide the Water Level. In fact, what the equipment provides is a counter for the descaling of the machine, meaning that when the countdown value reaches zero, the machine needs maintenance and decalcification. To calculate the water level, when the tank is refilled, a value in ml is persisted and for each decrement, an estimated value is subtracted, giving the real ml value at each moment.</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/posts/20260413_ConnectIoT_DidYouKnowMESCanTakeACoffee/WaterLevel.png"
    alt="WaterLevel"
    title="Water Level Workflow"
  />
  <figcaption style="text-align: center">Water Level Workflow</figcaption>
</figure>

</p>
<p>The calculated Water Level is then written to the Water level KPI.</p>
<h2 id="conclusion">Conclusion</h2>
<p>A Nespresso machine brewing espresso is about as far from a factory floor as you can get, and yet, with ConnectIoT and Critical Manufacturing MES, it fits naturally into the same integration model used for industrial equipment.</p>
<p>The BLE communication layer handles the protocol specifics, the Controller workflows manage the machine lifecycle, and MES takes care of the rest: material tracking, resource state, and production records. The coffee machine becomes, effectively, just another resource on the shop floor.</p>
<p>The exercise has a practical point. If MES can track a capsule from the moment it drops into the machine to the moment it ends up in the bin, it can track anything. The same patterns, event-driven state transitions, material dispatch and trackout, water consumption as a production variable, apply directly to real manufacturing scenarios.</p>
<p>The Nespresso PoC is a useful demonstration precisely because it is simple enough, yet covers enough ground to validate the integration approach. Next time someone asks you what ConnectIoT can do, the answer might just be: &ldquo;It can make a coffee&rdquo;.</p>
<h2 id="author">Author</h2>
<p>Hello, my name is Rodrigo Cordeiro. I’m passionate about machine integration, automation and transforming industrial data into clear, useful, decision‑ready insights.</p>
<p>You can reach me on <a href="https://www.linkedin.com/in/rodrigocordeiro/" target="_blank" rel="noopener">LinkedIn</a></p>
<p>Skills: ConnectIoT | Data Analyst | Data Scientist</p>
<p>
<figure class="text-center">
  <img
    src="/blogPosts/contributors/blogger_rodrigo_cordeiro.jpg"
    alt="Rodrigo Cordeiro"
    title="Connect IoT Engineer"
  />
  <figcaption style="text-align: center">Connect IoT Engineer</figcaption>
</figure>

</p>
]]></content:encoded></item></channel></rss>