From 8d97e2f7f2b18af3e5c9ce7be7fe591c8a0c473e Mon Sep 17 00:00:00 2001 From: Michael Hoffmeister Date: Sun, 7 Jan 2024 15:38:48 +0100 Subject: [PATCH] * investigate dll error for MQTT --- src/AasxIntegrationBase/LogInstance.cs | 23 +++ .../AasxPluginAssetInterfaceDesc.csproj | 5 +- .../AidInterfaceService.cs | 74 +++++++++ .../AidInterfaceStatus.cs | 100 +++++++++-- .../AidMqttConnection.cs | 155 ++++++++++++++++++ .../AssetInterfaceAnyUiControl.cs | 121 +++++++++++--- src/AasxPluginAssetInterfaceDesc/Plugin.cs | 5 + .../ShelfAnyUiControl.cs | 1 - 8 files changed, 450 insertions(+), 34 deletions(-) create mode 100644 src/AasxPluginAssetInterfaceDesc/AidInterfaceService.cs create mode 100644 src/AasxPluginAssetInterfaceDesc/AidMqttConnection.cs diff --git a/src/AasxIntegrationBase/LogInstance.cs b/src/AasxIntegrationBase/LogInstance.cs index 27facca19..6e221dc93 100644 --- a/src/AasxIntegrationBase/LogInstance.cs +++ b/src/AasxIntegrationBase/LogInstance.cs @@ -108,6 +108,29 @@ public StoredPrint( { return String.Format("{0}:{1} {2}", color, msg, linkTxt); } + + /// + /// Item1 = Foreground, Item2 = Background. + /// + public static Tuple LightThemeColor (Color color) + { + // https://coolors.co/palette/ffadad-ffd6a5-fdffb6-caffbf-9bf6ff-a0c4ff-bdb2ff-ffc6ff-fffffc + switch (color) + { + case Color.Blue: + return new Tuple(0xFF000000, 0xFFA0C4FF); + + case Color.Yellow: + return new Tuple(0xFF000000, 0xFFFDFFB6); + + case Color.Red: + return new Tuple(0xFF000000, 0xFFFFADAD); + + case Color.Black: + default: + return new Tuple(0xFF000000, 0xFFFFFFFF); + } + } } public class StoredPrintsMinimalStore diff --git a/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj b/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj index 1041bb6dc..893465f24 100644 --- a/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj +++ b/src/AasxPluginAssetInterfaceDesc/AasxPluginAssetInterfaceDesc.csproj @@ -16,10 +16,10 @@ - + PreserveNewest - + PreserveNewest @@ -46,6 +46,7 @@ + diff --git a/src/AasxPluginAssetInterfaceDesc/AidInterfaceService.cs b/src/AasxPluginAssetInterfaceDesc/AidInterfaceService.cs new file mode 100644 index 000000000..be00498a6 --- /dev/null +++ b/src/AasxPluginAssetInterfaceDesc/AidInterfaceService.cs @@ -0,0 +1,74 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using AasxIntegrationBase; +using AasxIntegrationBaseGdi; +using AdminShellNS; +using AnyUi; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; + +namespace AasxPluginAssetInterfaceDescription +{ + /// + /// This is thought as ONE service per ONE instance of AASX PE / BlazorExplorer + /// to care for all (continous) interfacing. + /// + public class AidInterfaceService + { + // + // Start of service + // + + private LogInstance _log = new LogInstance(); + + private const int _timerTickMs = 200; + private System.Timers.Timer _dispatcherTimer = null; + + private AidAllInterfaceStatus _allInterfaceStatus = null; + + public void StartOperation(LogInstance log, AidAllInterfaceStatus allInterfaceStatus) + { + _log = log; + _allInterfaceStatus = allInterfaceStatus; + + _dispatcherTimer = new System.Timers.Timer(_timerTickMs); + _dispatcherTimer.Elapsed += DispatcherTimer_Tick; + _dispatcherTimer.Enabled = true; + _dispatcherTimer.Start(); + } + + // + // Service + // + + private bool _inDispatcherTimer = false; + + private void DispatcherTimer_Tick(object sender, EventArgs e) + { + // access + if (_allInterfaceStatus == null || _inDispatcherTimer) + return; + + // block + _inDispatcherTimer = true; + + // .. + + // release mutex + _inDispatcherTimer = false; + + } + } +} diff --git a/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs b/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs index dbfd35dc3..a5a0e9fa3 100644 --- a/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs +++ b/src/AasxPluginAssetInterfaceDesc/AidInterfaceStatus.cs @@ -93,6 +93,16 @@ public class AidInterfaceStatus /// public string EndpointBase = ""; + /// + /// Actual summary of the status of the interface. + /// + public string LogLine = ""; + + /// + /// Black = idle, Blue = active, Red = error. + /// + public StoredPrint.Color LogColor = StoredPrint.Color.Black; + /// /// Link to entity (interface). /// @@ -127,6 +137,11 @@ virtual public void Close() virtual public void UpdateItemValue(AidIfxItemStatus item) { } + + virtual public void PrepareContinousRun(IEnumerable items) + { + + } } public class AidGenericConnections : Dictionary where T : AidBaseConnection, new() @@ -151,7 +166,13 @@ public T GetOrCreate(string target) /// public class AidAllInterfaceStatus { - public bool[] UseTech = { true, false, false }; + public bool[] UseTech = { false, false, true }; + + /// + /// Will hold connections steady and continously update values, either by + /// timer pased polling or by subscriptions. + /// + public bool ContinousRun = false; public List InterfaceStatus = new List(); @@ -161,11 +182,40 @@ public class AidAllInterfaceStatus public AidGenericConnections ModbusConnections = new AidGenericConnections(); + public AidGenericConnections MqttConnections = + new AidGenericConnections(); + + protected AidBaseConnection GetOrCreate(AidInterfaceTechnology tech, string endpointBase) + { + // find connection by factory + AidBaseConnection conn = null; + switch (tech) + { + case AidInterfaceTechnology.HTTP: + conn = HttpConnections.GetOrCreate(endpointBase); + break; + + case AidInterfaceTechnology.Modbus: + conn = ModbusConnections.GetOrCreate(endpointBase); + break; + + case AidInterfaceTechnology.MQTT: + conn = MqttConnections.GetOrCreate(endpointBase); + break; + } + return conn; + } + /// /// Will connect to each target once, get values and will disconnect again. /// public void UpdateValuesSingleShot() { + // access allowed + if (ContinousRun) + return; + + // for all foreach (var tech in AdminShellUtil.GetEnumValues()) { // use? @@ -180,17 +230,7 @@ public void UpdateValuesSingleShot() continue; // find connection by factory - AidBaseConnection conn = null; - switch (tech) - { - case AidInterfaceTechnology.HTTP: - conn = HttpConnections.GetOrCreate(ifc.EndpointBase); - break; - - case AidInterfaceTechnology.Modbus: - conn = ModbusConnections.GetOrCreate(ifc.EndpointBase); - break; - } + AidBaseConnection conn = GetOrCreate(tech, ifc.EndpointBase); if (conn == null) continue; @@ -212,6 +252,42 @@ public void UpdateValuesSingleShot() ifc.Connection.Close(); } } + + /// + /// Will connect to each target, leave the connection open, will enable + /// cyclic updates. + /// + public void StartContinousRun() + { + // for all + foreach (var tech in AdminShellUtil.GetEnumValues()) + { + // use? + if (!UseTech[(int)tech]) + continue; + + // find all interfaces with that technology + foreach (var ifc in InterfaceStatus.Where((i) => i.Technology == tech)) + { + // get a connection + if (ifc.EndpointBase?.HasContent() != true) + continue; + + // find connection by factory + AidBaseConnection conn = GetOrCreate(tech, ifc.EndpointBase); + if (conn == null) + continue; + + // open it + if (!conn.Open()) + continue; + ifc.Connection = conn; + + // start subscriptions .. + conn.PrepareContinousRun(ifc.Items); + } + } + } } } diff --git a/src/AasxPluginAssetInterfaceDesc/AidMqttConnection.cs b/src/AasxPluginAssetInterfaceDesc/AidMqttConnection.cs new file mode 100644 index 000000000..b8c64e6ab --- /dev/null +++ b/src/AasxPluginAssetInterfaceDesc/AidMqttConnection.cs @@ -0,0 +1,155 @@ +/* +Copyright (c) 2018-2023 Festo SE & Co. KG +Author: Michael Hoffmeister + +This source code is licensed under the Apache License 2.0 (see LICENSE.txt). + +This source code may use other Open Source software components (see LICENSE.txt). +*/ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using AasxPredefinedConcepts; +using Aas = AasCore.Aas3_0; +using AdminShellNS; +using Extensions; +using WpfMtpControl; +using AasxIntegrationBase; +using AasxPredefinedConcepts.AssetInterfacesDescription; +using FluentModbus; +using System.Net; +using System.Text.RegularExpressions; +using System.Globalization; +using System.Net.Http; +using MQTTnet; +using MQTTnet.Client; +using System.Web.Services.Description; + +namespace AasxPluginAssetInterfaceDescription +{ + public class AidMqttConnection : AidBaseConnection + { + protected static MqttFactory _factory = new MqttFactory(); + + public IMqttClient Client; + + protected Dictionary _subscribedTopics = new Dictionary(); + + override public bool Open() + { + try + { + // see: https://www.emqx.com/en/blog/connecting-to-serverless-mqtt-broker-with-mqttnet-in-csharp + Client = _factory.CreateMqttClient(); + + var options = new MqttClientOptionsBuilder() + .WithTcpServer(TargetUri.Host, TargetUri.Port) // MQTT broker address and port + // .WithCredentials(username, password) // Set username and password + .WithClientId("AasxPackageExplorer") + .WithCleanSession() + .Build(); + + // need to switch to async + var task = Task.Run(() => Client.ConnectAsync(options)); + task.Wait(); + var res = task.Result; + + // no subscriptions, yet + _subscribedTopics.Clear(); + + // get messages + Client.ApplicationMessageReceivedAsync += Client_ApplicationMessageReceivedAsync; + + // ok + return Client.IsConnected; + } + catch (Exception ex) + { + Client = null; + _subscribedTopics.Clear(); + return false; + } + } + + private async Task Client_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg) + { + // access + if (arg == null) + return; + + // topic? + var topic = arg.ApplicationMessage?.Topic; + if (topic?.HasContent() != true) + return; + + // payload? + var payload = Encoding.UTF8.GetString(arg.ApplicationMessage.PayloadSegment); + if (payload?.HasContent() != true) + return; + + // refer further.. + ; + + // ok + await Task.Yield(); + } + + override public bool IsConnected() + { + // simple + return Client != null && Client.IsConnected; + } + + override public void Close() + { + if (IsConnected()) + { + var task = Task.Run(() => Client.DisconnectAsync()); + task.Wait(); + _subscribedTopics.Clear(); + } + } + + override public void UpdateItemValue(AidIfxItemStatus item) + { + // Cannot do anything. MQTT is pure publish/ subscribe. + // Unable to ask for a status value. + } + + override public void PrepareContinousRun(IEnumerable items) + { + // access + if (!IsConnected() || items == null) + return; + + foreach (var item in items) + { + // valid topic? + var topic = "" + item.FormData?.Href; + if (topic.StartsWith("/")) + topic = topic.Remove(0, 1); + if (!topic.HasContent()) + continue; + + // need only "subscribe" + if (item.FormData?.Mqv_controlPacket?.HasContent() != true) + continue; + if (item.FormData.Mqv_controlPacket.Trim().ToLower() != "subscribe") + continue; + + // is topic already subscribed? + if (_subscribedTopics.ContainsKey(topic)) + continue; + + // ok, subscribe + var task = Task.Run(() => Client.SubscribeAsync(topic)); + task.Wait(); + _subscribedTopics.Add(topic, topic); + } + } + + } +} diff --git a/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs b/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs index 2f4cef29d..f2781a91c 100644 --- a/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs +++ b/src/AasxPluginAssetInterfaceDesc/AssetInterfaceAnyUiControl.cs @@ -217,7 +217,7 @@ protected void RenderPanelOutside( var inner = new AnyUiStackPanel() { Orientation = AnyUiOrientation.Vertical, - Margin = new AnyUiThickness(2) + Margin = new AnyUiThickness(2, 2, 8, 2) }; scroll.Content = inner; @@ -231,6 +231,18 @@ protected void RenderPanelOutside( #region Inner //============= + protected AnyUiLambdaActionBase TriggerUpdate(bool full = true) + { + // trigger a complete redraw, as the regions might emit + // events or not, depending on this flag + return new AnyUiLambdaActionPluginUpdateAnyUi() + { + PluginName = _plugin?.GetPluginName(), + UpdateMode = AnyUiRenderMode.All, + UseInnerGrid = true + }; + } + protected void RenderPanelInner( AnyUiStackPanel view, AnyUiSmallWidgetToolkit uitk, AssetInterfaceOptionsRecord rec, @@ -241,7 +253,7 @@ protected void RenderPanelInner( if (view == null || uitk == null || sm == null || rec == null) return; - var grid = view.Add(uitk.AddSmallGrid(rows: 3, cols: 2, colWidths: new[] { "110:", "*" })); + var grid = view.Add(uitk.AddSmallGrid(rows: 5, cols: 2, colWidths: new[] { "110:", "*" })); // // Technologies @@ -278,17 +290,19 @@ protected void RenderPanelInner( { try { + // locked? + if (_allInterfaceStatus?.ContinousRun == true) + { + _log.Info(StoredPrint.Color.Blue, "Not possible. Interfaces are in continous mode."); + return new AnyUiLambdaActionNone(); + } + // build up data structures _allInterfaceStatus.InterfaceStatus = PrepareAidInformation(sm); // trigger a complete redraw, as the regions might emit // events or not, depending on this flag - return new AnyUiLambdaActionPluginUpdateAnyUi() - { - PluginName = _plugin?.GetPluginName(), - UpdateMode = AnyUiRenderMode.All, - UseInnerGrid = true - }; + return TriggerUpdate(full: true); } catch (Exception ex) { @@ -306,20 +320,65 @@ protected void RenderPanelInner( { try { - //var client = new ModbusTcpClient(); - //client.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 5020)); - //var byteData = client.ReadHoldingRegisters(99, 1, 8); + // locked? + if (_allInterfaceStatus?.ContinousRun == true) + { + _log.Info(StoredPrint.Color.Blue, "Not possible. Interfaces are in continous mode."); + return new AnyUiLambdaActionNone(); + } + // single shot _allInterfaceStatus?.UpdateValuesSingleShot(); // trigger a complete redraw, as the regions might emit // events or not, depending on this flag - return new AnyUiLambdaActionPluginUpdateAnyUi() + return TriggerUpdate(full: true); + } + catch (Exception ex) + { + ; + } + return new AnyUiLambdaActionNone(); + }); + + AnyUiUIElement.RegisterControl( + uitk.AddSmallButtonTo(grid, 3, 1, + margin: new AnyUiThickness(2), setHeight: 21, + padding: new AnyUiThickness(2, 0, 2, 0), + content: "Start continous run .."), + (o) => + { + try + { + if (_allInterfaceStatus != null) { - PluginName = _plugin?.GetPluginName(), - UpdateMode = AnyUiRenderMode.All, - UseInnerGrid = true - }; + _allInterfaceStatus.StartContinousRun(); + } + + // trigger a complete redraw, as the regions might emit + // events or not, depending on this flag + return TriggerUpdate(full: true); + } + catch (Exception ex) + { + ; + } + return new AnyUiLambdaActionNone(); + }); + + AnyUiUIElement.RegisterControl( + uitk.AddSmallButtonTo(grid, 4, 1, + margin: new AnyUiThickness(2), setHeight: 21, + padding: new AnyUiThickness(2, 0, 2, 0), + content: "Stop continous run .."), + (o) => + { + try + { + + // trigger a complete redraw, as the regions might emit + // events or not, depending on this flag + return TriggerUpdate(full: true); } catch (Exception ex) { @@ -435,7 +494,10 @@ protected void RenderTripleRowData( int rowIndex = 0; foreach (var ifx in interfaces) { + // // heading + // + grid.RowDefinitions.Add(new AnyUiRowDefinition()); var headGrid = uitk.Set( @@ -446,7 +508,7 @@ protected void RenderTripleRowData( if (_dictTechnologyToBitmap.ContainsKey(ifx.Technology)) uitk.AddSmallImageTo(headGrid, 0, 0, - margin: new AnyUiThickness(0, 0, 10, 0), + margin: new AnyUiThickness(0, 4, 10, 4), bitmap: _dictTechnologyToBitmap[ifx.Technology]); uitk.AddSmallBasicLabelTo(headGrid, 0, 1, fontSize: 1.2f, setBold: true, @@ -460,7 +522,10 @@ protected void RenderTripleRowData( verticalContentAlignment: AnyUiVerticalAlignment.Center, content: ifx.Info); - // items? + // + // items + // + if (ifx.Items != null) foreach (var item in ifx.Items) { @@ -483,7 +548,25 @@ protected void RenderTripleRowData( }; } rowIndex++; - } + } + + // + // LogLine + // + + grid.RowDefinitions.Add(new AnyUiRowDefinition()); + + var clr = StoredPrint.LightThemeColor(ifx.LogColor); + + var ll = uitk.Set( + uitk.AddSmallBasicLabelTo(grid, rowIndex++, 0, fontSize: 0.8, + verticalAlignment: AnyUiVerticalAlignment.Center, + verticalContentAlignment: AnyUiVerticalAlignment.Center, + background: new AnyUiBrush(clr.Item2), + foreground: new AnyUiBrush(clr.Item1), + margin: new AnyUiThickness(0, 4, 0, 8), + content: "" + ifx.LogLine), + colSpan: 5); } } diff --git a/src/AasxPluginAssetInterfaceDesc/Plugin.cs b/src/AasxPluginAssetInterfaceDesc/Plugin.cs index 8f6f15c7b..df1500401 100644 --- a/src/AasxPluginAssetInterfaceDesc/Plugin.cs +++ b/src/AasxPluginAssetInterfaceDesc/Plugin.cs @@ -32,6 +32,7 @@ private AasxPluginAssetInterfaceDescription.AssetInterfaceOptions _options // TODO: make this multi-session!! private AidAllInterfaceStatus _allInterfaceStatus = new AidAllInterfaceStatus(); + private AidInterfaceService _interfaceService = null; public class Session : PluginSessionBase { @@ -63,6 +64,10 @@ public class Session : PluginSessionBase // index them! _options.IndexListOfRecords(_options.Records); + + // start interface service + _interfaceService = new AidInterfaceService(); + _interfaceService.StartOperation(_log, _allInterfaceStatus); } public new object CheckForLogMessage() diff --git a/src/AasxPluginDocumentShelf/ShelfAnyUiControl.cs b/src/AasxPluginDocumentShelf/ShelfAnyUiControl.cs index f25cd5255..c9fda5be5 100644 --- a/src/AasxPluginDocumentShelf/ShelfAnyUiControl.cs +++ b/src/AasxPluginDocumentShelf/ShelfAnyUiControl.cs @@ -131,7 +131,6 @@ public ShelfAnyUiControl() CurrInst = "" + InstCounter; InstCounter++; - } public void Dispose()