--- a/ConfigTreeNode.py Tue Aug 20 01:51:08 2024 +0200
+++ b/ConfigTreeNode.py Sat Sep 07 12:50:57 2024 +0200
@@ -139,6 +139,7 @@
if appframe is not None:
appframe.RefreshTitle()
appframe.RefreshPageTitles()
+ appframe.RefreshFileMenu()
def ProjectTestModified(self):
"""
--- a/exemples/first_steps/plc.xml Tue Aug 20 01:51:08 2024 +0200
+++ b/exemples/first_steps/plc.xml Sat Sep 07 12:50:57 2024 +0200
@@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
<fileHeader companyName="Beremiz" productName="Beremiz" productVersion="1" creationDateTime="2016-10-24T18:09:22"/>
- <contentHeader name="First Steps" modificationDateTime="2018-09-26T12:52:51">
+ <contentHeader name="First Steps" modificationDateTime="2024-07-12T16:04:57">
<coordinateInfo>
<fbd>
<scaling x="0" y="0"/>
--- a/features.py Tue Aug 20 01:51:08 2024 +0200
+++ b/features.py Sat Sep 07 12:50:57 2024 +0200
@@ -15,6 +15,7 @@
('SVGHMI', 'svghmi.SVGHMILibrary', 'svghmi')]
catalog = [
+ ('mqtt', _('MQTT client'), _('Map MQTT topics as located variables'), 'mqtt.MQTTClient'),
('opcua', _('OPC-UA client'), _('Map OPC-UA server as located variables'), 'opc_ua.OPCUAClient'),
# FIXME ('canfestival', _('CANopen support'), _('Map located variables over CANopen'), 'canfestival.canfestival.RootClass'),
('bacnet', _('Bacnet support'), _('Map located variables over Bacnet'), 'bacnet.bacnet.RootClass'),
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/__init__.py Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,6 @@
+# mqtt/__init__.py
+
+from __future__ import absolute_import
+
+from .client import MQTTClient
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/client.py Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,196 @@
+# mqtt/client.py
+
+from __future__ import absolute_import
+
+import os
+import re
+
+from editors.ConfTreeNodeEditor import ConfTreeNodeEditor
+from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT
+from .mqtt_client_gen import MQTTClientPanel, MQTTClientModel, MQTT_IEC_types, authParams
+
+import util.paths as paths
+
+
+# assumes that "build" directory was created in paho.mqtt.c source directory,
+# and cmake build was invoked from this directory
+PahoMqttCLibraryPath = paths.ThirdPartyPath("paho.mqtt.c", "build", "src")
+
+PahoMqttCIncludePaths = [
+ paths.ThirdPartyPath("paho.mqtt.c", "build"), # VersionInfo.h
+ paths.ThirdPartyPath("paho.mqtt.c", "src")
+]
+
+class MQTTClientEditor(ConfTreeNodeEditor):
+ CONFNODEEDITOR_TABS = [
+ (_("MQTT Client"), "CreateMQTTClient_UI")]
+
+ def Log(self, msg):
+ self.Controler.GetCTRoot().logger.write(msg)
+
+ def CreateMQTTClient_UI(self, parent):
+ return MQTTClientPanel(parent, self.Controler.GetModelData(), self.Log, self.Controler.GetConfig)
+
+class MQTTClient(object):
+ XSD = """<?xml version="1.0" encoding="ISO-8859-1" ?>
+ <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+ <xsd:element name="MQTTClient">
+ <xsd:complexType>
+ <xsd:sequence>
+ <xsd:element name="AuthType" minOccurs="0">
+ <xsd:complexType>
+ <xsd:choice minOccurs="0">
+ <xsd:element name="x509">
+ <xsd:complexType>
+ <xsd:attribute name="Client_certificate" type="xsd:string" use="optional" default="KeyStore.pem"/>
+ <xsd:attribute name="Broker_certificate" type="xsd:string" use="optional" default="TrustStore.pem"/>
+ <xsd:attribute name="Verify_hostname" type="xsd:boolean" use="optional" default="true"/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="PSK">
+ <xsd:complexType>
+ <xsd:attribute name="Secret" type="xsd:string" use="optional" default=""/>
+ <xsd:attribute name="ID" type="xsd:string" use="optional" default=""/>
+ </xsd:complexType>
+ </xsd:element>
+ <xsd:element name="UserPassword">
+ <xsd:complexType>
+ <xsd:attribute name="User" type="xsd:string" use="optional"/>
+ <xsd:attribute name="Password" type="xsd:string" use="optional"/>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:choice>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:sequence>
+ <xsd:attribute name="Use_MQTT_5" type="xsd:boolean" use="optional" default="true"/>
+ <xsd:attribute name="Broker_URI" type="xsd:string" use="optional" default="ws://localhost:1883"/>
+ <xsd:attribute name="Client_ID" type="xsd:string" use="optional" default=""/>
+ </xsd:complexType>
+ </xsd:element>
+ </xsd:schema>
+ """
+
+ EditorType = MQTTClientEditor
+
+ def __init__(self):
+ self.modeldata = MQTTClientModel(self.Log, self.CTNMarkModified)
+
+ filepath = self.GetFileName()
+ if os.path.isfile(filepath):
+ self.modeldata.LoadCSV(filepath)
+
+ def Log(self, msg):
+ self.GetCTRoot().logger.write(msg)
+
+ def GetModelData(self):
+ return self.modeldata
+
+ def GetConfig(self):
+ def cfg(path):
+ try:
+ attr=self.GetParamsAttributes("MQTTClient."+path)
+ except ValueError:
+ return None
+ return attr["value"]
+
+ AuthType = cfg("AuthType")
+ res = dict(
+ URI=cfg("Broker_URI"),
+ AuthType=AuthType,
+ clientID=cfg("Client_ID"),
+ UseMQTT5=cfg("Use_MQTT_5"))
+
+ paramList = authParams.get(AuthType, None)
+ if paramList:
+ for name,default in paramList:
+
+ # Translate internal config naming into user config naming
+ displayed_name = {"KeyStore" : "Client_certificate",
+ "TrustStore" : "Broker_certificate",
+ "Verify" : "Verify_hostname"}.get(name, name)
+
+ value = cfg("AuthType." + displayed_name)
+ if value == "" or value is None:
+ value = default
+
+ if value is not None:
+ # cryptomaterial is expected to be in project's user provided file directory
+
+ # User input may contain char incompatible with C string literal escaping
+ if name in ["User","Password","TrustStore","KeyStore","Broker_URI","Client_ID"]:
+ value = re.sub(r'([\"\\])', r'\\\1', value)
+
+ res[name] = value
+
+ return res
+
+ def GetFileName(self):
+ return os.path.join(self.CTNPath(), 'selected.csv')
+
+ def OnCTNSave(self, from_project_path=None):
+ self.modeldata.SaveCSV(self.GetFileName())
+ return True
+
+ def CTNGenerate_C(self, buildpath, locations):
+ current_location = self.GetCurrentLocation()
+ locstr = "_".join(map(str, current_location))
+ c_path = os.path.join(buildpath, "mqtt_client__%s.c" % locstr)
+
+ c_code = """
+#include "iec_types_all.h"
+#include "beremiz.h"
+"""
+ config = self.GetConfig()
+ c_code += self.modeldata.GenerateC(c_path, locstr, config)
+
+ with open(c_path, 'w') as c_file:
+ c_file.write(c_code)
+
+ if config["AuthType"] == "x509":
+ static_lib = "libpaho-mqtt3cs.a"
+ libs = ['-lssl', '-lcrypto']
+ else:
+ static_lib = "libpaho-mqtt3c.a"
+ libs = []
+
+ LDFLAGS = [' "' + os.path.join(PahoMqttCLibraryPath, static_lib) + '"'] + libs
+
+ CFLAGS = ' '.join(['-I"' + path + '"' for path in PahoMqttCIncludePaths])
+
+ return [(c_path, CFLAGS)], LDFLAGS, True
+
+ def GetVariableLocationTree(self):
+ current_location = self.GetCurrentLocation()
+ locstr = "_".join(map(str, current_location))
+ name = self.BaseParams.getName()
+
+ entries = []
+ children = []
+
+ for row in self.modeldata["output"]:
+ Topic, QoS, _Retained, iec_type, iec_number = row
+ entries.append((Topic, QoS, iec_type, iec_number, "Q", LOCATION_VAR_OUTPUT))
+
+ for row in self.modeldata["input"]:
+ Topic, QoS, iec_type, iec_number = row
+ entries.append((Topic, QoS, iec_type, iec_number, "I", LOCATION_VAR_INPUT))
+
+ for Topic, QoS, iec_type, iec_number, iec_dir_prefix, loc_type in entries:
+ C_type, iec_size_prefix = MQTT_IEC_types[iec_type]
+ c_loc_name = "__" + iec_dir_prefix + iec_size_prefix + locstr + "_" + str(iec_number)
+ children.append({
+ "name": Topic,
+ "type": loc_type,
+ "size": {"X":1, "B":8, "W":16, "D":32, "L":64}[iec_size_prefix],
+ "IEC_type": iec_type,
+ "var_name": c_loc_name,
+ "location": "%" + iec_dir_prefix + iec_size_prefix + ".".join([str(i) for i in current_location]) + "." + str(iec_number),
+ "description": "",
+ "children": []})
+
+ return {"name": name,
+ "type": LOCATION_CONFNODE,
+ "location": ".".join([str(i) for i in current_location]) + ".x",
+ "children": children}
+
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/mqtt_client_gen.py Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,543 @@
+from __future__ import print_function
+from __future__ import absolute_import
+
+import csv
+import functools
+from threading import Thread
+from collections import OrderedDict
+
+import wx
+import wx.dataview as dv
+
+import util.paths as paths
+
+# from perfect_hash import generate_code, IntSaltHash
+
+MQTT_IEC_types = dict(
+# IEC61131| C type | sz
+ BOOL = ("uint8_t" , "X"),
+ SINT = ("int8_t" , "B"),
+ USINT = ("uint8_t" , "B"),
+ INT = ("int16_t" , "W"),
+ UINT = ("uint16_t", "W"),
+ DINT = ("uint32_t", "D"),
+ UDINT = ("int32_t" , "D"),
+ LINT = ("int64_t" , "L"),
+ ULINT = ("uint64_t", "L"),
+ REAL = ("float" , "D"),
+ LREAL = ("double" , "L"),
+)
+
+"""
+ QoS - Quality of Service
+ 0 : "At most one delivery"
+ 1 : "At least one delivery"
+ 2 : "Exactly one delivery"
+"""
+QoS_values = [0, 1, 2]
+
+def boolean(v):
+ if v in ["False","0"]:
+ return False
+ else:
+ return bool(v)
+
+_lstcolnames = [ "Topic", "QoS", "Retained", "Type", "Location"]
+_lstcolwidths = [ 100, 50, 100, 100, 50]
+_lstcoltypess = [ str, int, boolean, str, int]
+_lstcoldeflts = [ "a/b/c", "1", False, "DINT", "0"]
+
+subsublist = lambda l : l[0:2] + l[3:5]
+
+lstcoldsc = {
+ "input" : type("",(),dict(
+ lstcolnames = subsublist(_lstcolnames),
+ lstcolwidths = subsublist(_lstcolwidths),
+ lstcoltypess = subsublist(_lstcoltypess),
+ lstcoldeflts = subsublist(_lstcoldeflts),
+ Location_column = 3)),
+ "output" : type("",(),dict(
+ lstcolnames = _lstcolnames,
+ lstcolwidths = _lstcolwidths,
+ lstcoltypess = _lstcoltypess,
+ lstcoldeflts = _lstcoldeflts,
+ Location_column = 4)),
+}
+
+directions = ["input", "output"]
+
+# expected configuration entries with internal default value
+authParams = {
+ "x509":[
+ ("Verify", True),
+ ("KeyStore", None),
+ ("TrustStore", None)],
+ "UserPassword":[
+ ("User", None),
+ ("Password", None)]}
+
+class MQTTTopicListModel(dv.PyDataViewIndexListModel):
+ def __init__(self, data, log, direction):
+ dv.PyDataViewIndexListModel.__init__(self, len(data))
+ self.data = data
+ self.log = log
+ self.dsc = lstcoldsc[direction]
+
+ def GetColumnType(self, col):
+ return "string"
+
+ def GetValueByRow(self, row, col):
+ return str(self.data[row][col])
+
+ # This method is called when the user edits a data item in the view.
+ def SetValueByRow(self, value, row, col):
+ expectedtype = self.dsc.lstcoltypess[col]
+
+ try:
+ v = expectedtype(value)
+ except ValueError:
+ self.log("String {} is invalid for type {}\n".format(value,expectedtype.__name__))
+ return False
+
+ if col == self.dsc.lstcolnames.index("QoS") and v not in QoS_values:
+ self.log("{} is invalid for IdType\n".format(value))
+ return False
+
+ line = self.data[row]
+ line[col] = v
+ self.data[row] = line
+ return True
+
+ # Report how many columns this model provides data for.
+ def GetColumnCount(self):
+ return len(self.dsc.lstcolnames)
+
+ # Report the number of rows in the model
+ def GetCount(self):
+ #self.log.write('GetCount')
+ return len(self.data)
+
+ # Called to check if non-standard attributes should be used in the
+ # cell at (row, col)
+ def GetAttrByRow(self, row, col, attr):
+ if col == self.dsc.Location_column:
+ attr.SetColour('blue')
+ attr.SetBold(True)
+ return True
+ return False
+
+
+ def DeleteRows(self, rows):
+ # make a copy since we'll be sorting(mutating) the list
+ # use reverse order so the indexes don't change as we remove items
+ rows = sorted(rows, reverse=True)
+
+ for row in rows:
+ # remove it from our data structure
+ del self.data[row]
+ # notify the view(s) using this model that it has been removed
+ self.RowDeleted(row)
+
+
+ def AddRow(self, value):
+ if self.data.append(value):
+ # notify views
+ self.RowAppended()
+
+ def InsertDefaultRow(self, row):
+ self.data.insert(row, self.dsc.lstcoldeflts[:])
+ # notify views
+ self.RowInserted(row)
+
+ def ResetData(self):
+ self.Reset(len(self.data))
+
+class MQTTTopicListPanel(wx.Panel):
+ def __init__(self, parent, log, model, direction):
+ self.log = log
+ wx.Panel.__init__(self, parent, -1)
+
+ self.dvc = dv.DataViewCtrl(self,
+ style=wx.BORDER_THEME
+ | dv.DV_ROW_LINES
+ | dv.DV_HORIZ_RULES
+ | dv.DV_VERT_RULES
+ | dv.DV_MULTIPLE
+ )
+
+ self.model = model
+
+ self.dvc.AssociateModel(self.model)
+
+ dsc = lstcoldsc[direction]
+ for idx,(colname,width) in enumerate(zip(dsc.lstcolnames,dsc.lstcolwidths)):
+ self.dvc.AppendTextColumn(colname, idx, width=width, mode=dv.DATAVIEW_CELL_EDITABLE)
+
+
+ self.Sizer = wx.BoxSizer(wx.VERTICAL)
+
+ self.direction = direction
+ titlestr = direction + " variables"
+
+ title = wx.StaticText(self, label = titlestr)
+
+ addbt = wx.Button(self, label="Add")
+ self.Bind(wx.EVT_BUTTON, self.OnAddRow, addbt)
+ delbt = wx.Button(self, label="Delete")
+ self.Bind(wx.EVT_BUTTON, self.OnDeleteRows, delbt)
+
+ topsizer = wx.BoxSizer(wx.HORIZONTAL)
+ topsizer.Add(title, 1, wx.ALIGN_CENTER_VERTICAL|wx.LEFT|wx.RIGHT, 5)
+ topsizer.Add(addbt, 0, wx.LEFT|wx.RIGHT, 5)
+ topsizer.Add(delbt, 0, wx.LEFT|wx.RIGHT, 5)
+ self.Sizer.Add(topsizer, 0, wx.EXPAND|wx.TOP|wx.BOTTOM, 5)
+ self.Sizer.Add(self.dvc, 1, wx.EXPAND)
+
+
+ def OnAddRow(self, evt):
+ items = self.dvc.GetSelections()
+ row = self.model.GetRow(items[0]) if items else 0
+ self.model.InsertDefaultRow(row)
+
+ def OnDeleteRows(self, evt):
+ items = self.dvc.GetSelections()
+ rows = [self.model.GetRow(item) for item in items]
+ self.model.DeleteRows(rows)
+
+
+class MQTTClientPanel(wx.SplitterWindow):
+ def __init__(self, parent, modeldata, log, config_getter):
+ self.log = log
+ wx.SplitterWindow.__init__(self, parent, style=wx.SUNKEN_BORDER | wx.SP_3D)
+
+ self.config_getter = config_getter
+
+ self.selected_datas = modeldata
+ self.selected_models = { direction:MQTTTopicListModel(
+ self.selected_datas[direction], log, direction) for direction in directions }
+ self.selected_lists = { direction:MQTTTopicListPanel(
+ self, log,
+ self.selected_models[direction], direction)
+ for direction in directions }
+
+ self.SplitHorizontally(*[self.selected_lists[direction] for direction in directions]+[300])
+
+ self.SetAutoLayout(True)
+
+ def OnClose(self):
+ pass
+
+ def __del__(self):
+ self.OnClose()
+
+ def Reset(self):
+ for direction in directions:
+ self.selected_models[direction].ResetData()
+
+
+class MQTTClientList(list):
+ def __init__(self, log, change_callback, direction):
+ super(MQTTClientList, self).__init__(self)
+ self.log = log
+ self.change_callback = change_callback
+ self.dsc = lstcoldsc[direction]
+
+ def _filter_line(self, value):
+ v = dict(list(zip(self.dsc.lstcolnames, value)))
+
+ if type(v["Location"]) != int:
+ if len(self) == 0:
+ v["Location"] = 0
+ else:
+ iecnums = set(zip(*self)[self.dsc.Location_column])
+ greatest = max(iecnums)
+ holes = set(range(greatest)) - iecnums
+ v["Location"] = min(holes) if holes else greatest+1
+
+ try:
+ for t,n in zip(self.dsc.lstcoltypess, self.dsc.lstcolnames):
+ v[n] = t(v[n])
+ except ValueError:
+ self.log("MQTT topic {} (Location={}) has invalid type\n".format(v["Topic"],v["Location"]))
+ return None
+
+ if v["QoS"] not in QoS_values:
+ self.log("Unknown QoS\n".format(value))
+ return None
+
+ if len(self)>0 and v["Topic"] in list(zip(*self))[self.dsc.lstcolnames.index("Topic")]:
+ self.log("MQTT topic {} (Location={}) already in the list\n".format(v["Topic"],v["Location"]))
+ return None
+
+ return [v[n] for n in self.dsc.lstcolnames]
+
+ def insert(self, row, value):
+ v = self._filter_line(value)
+ if v is not None:
+ list.insert(self, row, v)
+ self.change_callback()
+ return True
+ return False
+
+ def append(self, value):
+ v = self._filter_line(value)
+ if v is not None:
+ list.append(self, v)
+ self.change_callback()
+ return True
+ return False
+
+ def __setitem__(self, index, value):
+ list.__setitem__(self, index, value)
+ self.change_callback()
+
+ def __delitem__(self, index):
+ list.__delitem__(self, index)
+ self.change_callback()
+
+
+class MQTTClientModel(dict):
+ def __init__(self, log, change_callback = lambda : None):
+ super(MQTTClientModel, self).__init__()
+ for direction in directions:
+ self[direction] = MQTTClientList(log, change_callback, direction)
+
+ def LoadCSV(self,path):
+ with open(path, 'r') as csvfile:
+ reader = csv.reader(csvfile, delimiter=',', quotechar='"')
+ buf = {direction:[] for direction, _model in self.iteritems()}
+ for direction, model in self.iteritems():
+ self[direction][:] = []
+ for row in reader:
+ direction = row[0]
+ # avoids calling change callback when loading CSV
+ l = self[direction]
+ v = l._filter_line(row[1:])
+ if v is not None:
+ list.append(l,v)
+ # TODO be verbose in case of malformed CSV
+
+ def SaveCSV(self,path):
+ with open(path, 'w') as csvfile:
+ for direction, data in self.items():
+ writer = csv.writer(csvfile, delimiter=',',
+ quotechar='"', quoting=csv.QUOTE_MINIMAL)
+ for row in data:
+ writer.writerow([direction] + row)
+
+ def GenerateC(self, path, locstr, config):
+ c_template_filepath = paths.AbsNeighbourFile(__file__, "mqtt_template.c")
+ c_template_file = open(c_template_filepath , 'rb')
+ c_template = c_template_file.read()
+ c_template_file.close()
+
+ formatdict = dict(
+ locstr = locstr,
+ uri = config["URI"],
+ clientID = config["clientID"],
+ decl = "",
+ topics = "",
+ cleanup = "",
+ init = "",
+ init_pubsub = "",
+ retrieve = "",
+ publish = "",
+ publish_changes = ""
+ )
+
+
+ # Use Config's "MQTTVersion" to switch between protocol version at build time
+ if config["UseMQTT5"]:
+ formatdict["decl"] += """
+#define USE_MQTT_5""".format(**config)
+
+ AuthType = config["AuthType"]
+ print(config)
+ if AuthType == "x509":
+ for k in ["KeyStore","TrustStore"]:
+ config[k] = '"'+config[k]+'"' if config[k] else "NULL"
+ formatdict["init"] += """
+ INIT_x509({Verify:d}, {KeyStore}, {TrustStore})""".format(**config)
+ if AuthType == "PSK":
+ formatdict["init"] += """
+ INIT_PSK("{Secret}", "{ID}")""".format(**config)
+ elif AuthType == "UserPassword":
+ formatdict["init"] += """
+ INIT_UserPassword("{User}", "{Password}")""".format(**config)
+ else:
+ formatdict["init"] += """
+ INIT_NoAuth()"""
+
+ for row in self["output"]:
+ Topic, QoS, _Retained, iec_type, iec_number = row
+ Retained = 1 if _Retained=="True" else 0
+ C_type, iec_size_prefix = MQTT_IEC_types[iec_type]
+ c_loc_name = "__Q" + iec_size_prefix + locstr + "_" + str(iec_number)
+
+ formatdict["decl"] += """
+DECL_VAR({iec_type}, {C_type}, {c_loc_name})""".format(**locals())
+ formatdict["init_pubsub"] += """
+ INIT_PUBLICATION({Topic}, {QoS}, {C_type}, {c_loc_name}, {Retained})""".format(**locals())
+ formatdict["publish"] += """
+ WRITE_VALUE({c_loc_name}, {C_type})""".format(**locals())
+ formatdict["publish_changes"] += """
+ PUBLISH_CHANGE({Topic}, {QoS}, {C_type}, {c_loc_name}, {Retained})""".format(**locals())
+
+ # inputs need to be sorted for bisection search
+ for row in sorted(self["input"]):
+ Topic, QoS, iec_type, iec_number = row
+ C_type, iec_size_prefix = MQTT_IEC_types[iec_type]
+ c_loc_name = "__I" + iec_size_prefix + locstr + "_" + str(iec_number)
+ formatdict["decl"] += """
+DECL_VAR({iec_type}, {C_type}, {c_loc_name})""".format(**locals())
+ formatdict["topics"] += """
+ INIT_TOPIC({Topic}, {iec_type}, {c_loc_name})""".format(**locals())
+ formatdict["init_pubsub"] += """
+ INIT_SUBSCRIPTION({Topic}, {QoS})""".format(**locals())
+ formatdict["retrieve"] += """
+ READ_VALUE({c_loc_name}, {C_type})""".format(**locals())
+
+ Ccode = c_template.format(**formatdict)
+
+ return Ccode
+
+if __name__ == "__main__":
+
+ import wx.lib.mixins.inspection as wit
+ import sys,os
+
+ app = wit.InspectableApp()
+
+ frame = wx.Frame(None, -1, "MQTT Client Test App", size=(800,600))
+
+ argc = len(sys.argv)
+
+ config={}
+ config["URI"] = sys.argv[1] if argc>1 else "tcp://localhost:1883"
+ config["clientID"] = sys.argv[2] if argc>2 else ""
+ config["AuthType"] = None
+ config["UseMQTT5"] = True
+
+ if argc > 3:
+ AuthType = sys.argv[3]
+ config["AuthType"] = AuthType
+ for (name, default), value in zip_longest(authParams[AuthType], sys.argv[4:]):
+ if value is None:
+ if default is None:
+ raise Exception(name+" param expected")
+ value = default
+ config[name] = value
+
+ test_panel = wx.Panel(frame)
+ test_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
+ test_sizer.AddGrowableCol(0)
+ test_sizer.AddGrowableRow(0)
+
+ modeldata = MQTTClientModel(print)
+
+ mqtttestpanel = MQTTClientPanel(test_panel, modeldata, print, lambda:config)
+
+ def OnGenerate(evt):
+ dlg = wx.FileDialog(
+ frame, message="Generate file as ...", defaultDir=os.getcwd(),
+ defaultFile="",
+ wildcard="C (*.c)|*.c", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
+ )
+
+ if dlg.ShowModal() == wx.ID_OK:
+ path = dlg.GetPath()
+ Ccode = """
+/*
+In case open62541 was built just aside beremiz, you can build this test with:
+gcc %s -o %s \\
+ -I ../../open62541/plugins/include/ \\
+ -I ../../open62541/build/src_generated/ \\
+ -I ../../open62541/include/ \\
+ -I ../../open62541/arch/ ../../open62541/build/bin/libopen62541.a
+*/
+
+"""%(path, path[:-2]) + modeldata.GenerateC(path, "test", config) + """
+
+int LogMessage(uint8_t level, char* buf, uint32_t size){
+ printf("log level:%d message:'%.*s'\\n", level, size, buf);
+};
+
+int main(int argc, char *argv[]) {
+
+ __init_test(arc,argv);
+
+ __retrieve_test();
+
+ __publish_test();
+
+ __cleanup_test();
+
+ return EXIT_SUCCESS;
+}
+"""
+
+ with open(path, 'w') as Cfile:
+ Cfile.write(Ccode)
+
+
+ dlg.Destroy()
+
+ def OnLoad(evt):
+ dlg = wx.FileDialog(
+ frame, message="Choose a file",
+ defaultDir=os.getcwd(),
+ defaultFile="",
+ wildcard="CSV (*.csv)|*.csv",
+ style=wx.FD_OPEN | wx.FD_CHANGE_DIR | wx.FD_FILE_MUST_EXIST )
+
+ if dlg.ShowModal() == wx.ID_OK:
+ path = dlg.GetPath()
+ modeldata.LoadCSV(path)
+ mqtttestpanel.Reset()
+
+ dlg.Destroy()
+
+ def OnSave(evt):
+ dlg = wx.FileDialog(
+ frame, message="Save file as ...", defaultDir=os.getcwd(),
+ defaultFile="",
+ wildcard="CSV (*.csv)|*.csv", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT
+ )
+
+ if dlg.ShowModal() == wx.ID_OK:
+ path = dlg.GetPath()
+ modeldata.SaveCSV(path)
+
+ dlg.Destroy()
+
+ test_sizer.Add(mqtttestpanel, flag=wx.GROW|wx.EXPAND)
+
+ testbt_sizer = wx.BoxSizer(wx.HORIZONTAL)
+
+ loadbt = wx.Button(test_panel, label="Load")
+ test_panel.Bind(wx.EVT_BUTTON, OnLoad, loadbt)
+
+ savebt = wx.Button(test_panel, label="Save")
+ test_panel.Bind(wx.EVT_BUTTON, OnSave, savebt)
+
+ genbt = wx.Button(test_panel, label="Generate")
+ test_panel.Bind(wx.EVT_BUTTON, OnGenerate, genbt)
+
+ testbt_sizer.Add(loadbt, 0, wx.LEFT|wx.RIGHT, 5)
+ testbt_sizer.Add(savebt, 0, wx.LEFT|wx.RIGHT, 5)
+ testbt_sizer.Add(genbt, 0, wx.LEFT|wx.RIGHT, 5)
+
+ test_sizer.Add(testbt_sizer, flag=wx.GROW)
+ test_sizer.Layout()
+ test_panel.SetAutoLayout(True)
+ test_panel.SetSizer(test_sizer)
+
+ def OnClose(evt):
+ mqtttestpanel.OnClose()
+ evt.Skip()
+
+ frame.Bind(wx.EVT_CLOSE, OnClose)
+
+ frame.Show()
+
+ app.MainLoop()
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/mqtt_template.c Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,423 @@
+/* code generated by beremiz MQTT extension */
+
+#include <stdint.h>
+#include <unistd.h>
+#include <pthread.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "MQTTClient.h"
+#include "MQTTClientPersistence.h"
+
+#define _Log(level, ...) \
+ {{ \
+ char mstr[256]; \
+ snprintf(mstr, 255, __VA_ARGS__); \
+ LogMessage(level, mstr, strlen(mstr)); \
+ printf(__VA_ARGS__); \
+ fflush(stdout); \
+ }}
+
+#define LogInfo(...) _Log(LOG_INFO, __VA_ARGS__);
+#define LogError(...) _Log(LOG_CRITICAL, __VA_ARGS__);
+#define LogWarning(...) _Log(LOG_WARNING, __VA_ARGS__);
+
+// Selected debug level for paho stack
+// can be:
+// MQTTCLIENT_TRACE_PROTOCOL, MQTTCLIENT_TRACE_MAXIMUM, MQTTCLIENT_TRACE_ERROR
+#define MQTT_DEBUG_LEVEL MQTTCLIENT_TRACE_ERROR
+
+void trace_callback(enum MQTTCLIENT_TRACE_LEVELS level, char* message)
+{{
+ if(level >= MQTT_DEBUG_LEVEL)
+ {{
+ int beremiz_log_level = (level >= MQTTCLIENT_TRACE_ERROR ) ? LOG_CRITICAL :
+ (level > MQTTCLIENT_TRACE_MINIMUM) ? LOG_WARNING :
+ LOG_INFO;
+ _Log(beremiz_log_level,"Paho MQTT Trace : %s\n", message);
+ }}
+}}
+
+#define CHANGED 1
+#define UNCHANGED 0
+
+#define DECL_VAR(iec_type, C_type, c_loc_name) \
+static C_type PLC_##c_loc_name##_buf = 0; \
+static C_type MQTT_##c_loc_name##_buf = 0; \
+static int MQTT_##c_loc_name##_state = UNCHANGED; /* systematically published at init */ \
+C_type *c_loc_name = &PLC_##c_loc_name##_buf;
+
+{decl}
+
+static MQTTClient client;
+#ifdef USE_MQTT_5
+static MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer5;
+#else
+static MQTTClient_connectOptions conn_opts = MQTTClient_connectOptions_initializer;
+#endif
+
+MQTTClient_SSLOptions ssl_opts = MQTTClient_SSLOptions_initializer;
+
+/* condition to quit publish thread */
+static int MQTT_stop_thread = 0;
+
+/* condition to wakeup publish thread */
+static int MQTT_any_pub_var_changed = 0;
+
+/* Keep track of connection state */
+static volatile int MQTT_is_disconnected = 1;
+
+/* mutex to keep incoming PLC data consistent */
+static pthread_mutex_t MQTT_retrieve_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+/* mutex to keep outgoing PLC data consistent, and protect MQTT_any_pub_var_changed */
+static pthread_mutex_t MQTT_thread_wakeup_mutex = PTHREAD_MUTEX_INITIALIZER;
+
+/* wakeup publish thread when PLC changed published variable */
+static pthread_cond_t MQTT_thread_wakeup = PTHREAD_COND_INITIALIZER;
+
+/* thread that handles publish and reconnection */
+static pthread_t MQTT_thread;
+
+#define INIT_TOPIC(topic, iec_type, c_loc_name) \
+{{#topic, &MQTT_##c_loc_name##_buf, &MQTT_##c_loc_name##_state, iec_type##_ENUM}},
+
+static struct {{
+ const char *topic; //null terminated topic string
+ void *mqtt_pdata; // pointer to data from/for MQTT stack
+ int *mqtt_pchanged; // pointer to changed flag
+ __IEC_types_enum vartype;
+}} topics [] = {{
+{topics}
+}};
+
+void __cleanup_{locstr}(void)
+{{
+ int rc;
+
+ /* stop publish thread */
+ MQTT_stop_thread = 1;
+ if (pthread_mutex_lock(&MQTT_thread_wakeup_mutex) == 0){{
+ /* unblock publish thread so that it can stop normally */
+ pthread_cond_signal(&MQTT_thread_wakeup);
+ pthread_mutex_unlock(&MQTT_thread_wakeup_mutex);
+ }}
+ pthread_join(MQTT_thread, NULL);
+
+#ifdef USE_MQTT_5
+ if (rc = MQTTClient_disconnect5(client, 5000, MQTTREASONCODE_SUCCESS, NULL) != MQTTCLIENT_SUCCESS)
+#else
+ if (rc = MQTTClient_disconnect(client, 5000) != MQTTCLIENT_SUCCESS)
+#endif
+ {{
+ LogError("MQTT Failed to disconnect, return code %d\n", rc);
+ }}
+ MQTTClient_destroy(&client);
+}}
+
+void connectionLost(void* context, char* reason)
+{{
+ int rc;
+ LogWarning("ConnectionLost, reconnecting\\n");
+ if (pthread_mutex_lock(&MQTT_thread_wakeup_mutex) == 0){{
+ /* unblock publish thread so that it can reconnect */
+ MQTT_is_disconnected = 1;
+ pthread_cond_signal(&MQTT_thread_wakeup);
+ pthread_mutex_unlock(&MQTT_thread_wakeup_mutex);
+ }}
+}}
+
+
+
+int messageArrived(void *context, char *topicName, int topicLen, MQTTClient_message *message)
+{{
+ int low = 0;
+ int size = sizeof(topics) / sizeof(topics[0]);
+ int high = size - 1;
+ int mid;
+
+ // bisect topic among subscribed topics
+ while (low <= high) {{
+ int res;
+ mid = low + (high - low) / 2;
+ res = strncmp(topics[mid].topic, topicName, topicLen);
+
+ // Check if key is present at mid
+ if (res == 0)
+ goto found;
+
+ // If key greater, ignore left half
+ if (res < 0)
+ low = mid + 1;
+
+ // If key is smaller, ignore right half
+ else
+ high = mid - 1;
+ }}
+ // If we reach here, then the element was not present
+ LogWarning("MQTT unknown topic: %s", topicName);
+ goto exit;
+
+found:
+ if(__get_type_enum_size(topics[mid].vartype) == message->payloadlen){{
+ if (pthread_mutex_lock(&MQTT_retrieve_mutex) == 0){{
+ memcpy(topics[mid].mqtt_pdata, (char*)message->payload, message->payloadlen);
+ *topics[mid].mqtt_pchanged = 1;
+ pthread_mutex_unlock(&MQTT_retrieve_mutex);
+ }}
+ }} else {{
+ LogWarning("MQTT wrong payload size for topic: %s. Should be %d, but got %d.",
+ topicName, __get_type_enum_size(topics[mid].vartype), message->payloadlen);
+ }}
+exit:
+ MQTTClient_freeMessage(&message);
+ MQTTClient_free(topicName);
+ return 1;
+}}
+
+#define INIT_NoAuth() \
+ LogInfo("MQTT Init no auth\n");
+
+#define INIT_x509(Verify, KeyStore, TrustStore) \
+ LogInfo("MQTT Init x509 with %s,%s\n", KeyStore, TrustStore) \
+ ssl_opts.verify = Verify; \
+ ssl_opts.keyStore = KeyStore; \
+ ssl_opts.trustStore = TrustStore; \
+ conn_opts.ssl = &ssl_opts;
+
+#define INIT_PSK(Secret, ID) \
+ LogError("MQTT PSK NOT IMPLEMENTED\n") \
+ /* LogInfo("MQTT Init PSK for ID %s\n", ID) */ \
+ /* ssl_opts.ssl_psk_cb = TODO; */ \
+ /* ssl_opts.ssl_psk_context = TODO; */ \
+ conn_opts.ssl = &ssl_opts;
+
+#define INIT_UserPassword(User, Password) \
+ LogInfo("MQTT Init UserPassword %s,%s\n", User, Password); \
+ conn_opts.username = User; \
+ conn_opts.password = Password;
+
+#ifdef USE_MQTT_5
+#define _SUBSCRIBE(Topic, QoS) \
+ MQTTResponse response = MQTTClient_subscribe5(client, #Topic, QoS, NULL, NULL); \
+ /* when using MQTT5 responce code is 1 for some reason even if no error */ \
+ rc = response.reasonCode == 1 ? MQTTCLIENT_SUCCESS : response.reasonCode; \
+ MQTTResponse_free(response);
+#else
+#define _SUBSCRIBE(Topic, QoS) \
+ rc = MQTTClient_subscribe(client, #Topic, QoS);
+#endif
+
+#define INIT_SUBSCRIPTION(Topic, QoS) \
+ {{ \
+ int rc; \
+ _SUBSCRIBE(Topic, QoS) \
+ if (rc != MQTTCLIENT_SUCCESS) \
+ {{ \
+ LogError("MQTT client failed to subscribe to '%s', return code %d\n", #Topic, rc); \
+ }} \
+ }}
+
+
+#ifdef USE_MQTT_5
+#define _PUBLISH(Topic, QoS, C_type, c_loc_name, Retained) \
+ MQTTResponse response = MQTTClient_publish5(client, #Topic, sizeof(C_type), \
+ &MQTT_##c_loc_name##_buf, QoS, Retained, NULL, NULL); \
+ rc = response.reasonCode; \
+ MQTTResponse_free(response);
+#else
+#define _PUBLISH(Topic, QoS, C_type, c_loc_name, Retained) \
+ rc = MQTTClient_publish(client, #Topic, sizeof(C_type), \
+ &PLC_##c_loc_name##_buf, QoS, Retained, NULL);
+#endif
+
+#define INIT_PUBLICATION(Topic, QoS, C_type, c_loc_name, Retained) \
+ {{ \
+ int rc; \
+ _PUBLISH(Topic, QoS, C_type, c_loc_name, Retained) \
+ if (rc != MQTTCLIENT_SUCCESS) \
+ {{ \
+ LogError("MQTT client failed to init publication of '%s', return code %d\n", #Topic, rc);\
+ /* TODO update status variable accordingly */ \
+ }} \
+ }}
+
+#define PUBLISH_CHANGE(Topic, QoS, C_type, c_loc_name, Retained) \
+ if(MQTT_##c_loc_name##_state == CHANGED) \
+ {{ \
+ int rc; \
+ _PUBLISH(Topic, QoS, C_type, c_loc_name, Retained) \
+ if (rc != MQTTCLIENT_SUCCESS) \
+ {{ \
+ LogError("MQTT client failed to publish '%s', return code %d\n", #Topic, rc); \
+ /* TODO update status variable accordingly */ \
+ }} else {{ \
+ MQTT_##c_loc_name##_state = UNCHANGED; \
+ }} \
+ }}
+
+static int _connect_mqtt(void)
+{{
+ int rc;
+
+#ifdef USE_MQTT_5
+ MQTTProperties props = MQTTProperties_initializer;
+ MQTTProperties willProps = MQTTProperties_initializer;
+ MQTTResponse response = MQTTResponse_initializer;
+
+ response = MQTTClient_connect5(client, &conn_opts, &props, &willProps);
+ rc = response.reasonCode;
+ MQTTResponse_free(response);
+#else
+ rc = MQTTClient_connect(client, &conn_opts);
+#endif
+
+ if (rc != MQTTCLIENT_SUCCESS) {{
+ MQTT_is_disconnected = 1;
+ return rc;
+ }}else{{
+ MQTT_is_disconnected = 0;
+ }}
+
+{init_pubsub}
+
+ return MQTTCLIENT_SUCCESS;
+}}
+
+static void *__MQTT_thread_proc(void *_unused) {{
+ int rc = 0;
+
+ while((rc = pthread_mutex_lock(&MQTT_thread_wakeup_mutex)) == 0 && !MQTT_stop_thread){{
+ int do_publish;
+
+ pthread_cond_wait(&MQTT_thread_wakeup, &MQTT_thread_wakeup_mutex);
+
+ if(MQTT_is_disconnected)
+ {{
+ /* TODO growing retry delay */
+ /* TODO max retry delay as config parameter */
+ sleep(5);
+ rc = _connect_mqtt();
+ if (rc == MQTTCLIENT_SUCCESS) {{
+ LogInfo("MQTT Reconnected\n");
+ }} else {{
+ LogError("MQTT Reconnect Failed, return code %d\n", rc);
+ }}
+ }}
+ if(!MQTT_is_disconnected && MQTT_any_pub_var_changed)
+ {{
+ /* publish changes, and reset variable's state to UNCHANGED */
+{publish_changes}
+ MQTT_any_pub_var_changed = 0;
+ }}
+
+ pthread_mutex_unlock(&MQTT_thread_wakeup_mutex);
+
+ if(MQTT_stop_thread) break;
+ }}
+
+ if(!MQTT_stop_thread){{
+ /* if thread exits outside of normal shutdown, report error*/
+ LogError("MQTT client thread exited unexpectedly, return code %d\n", rc);
+ }}
+}}
+
+int __init_{locstr}(int argc,char **argv)
+{{
+ char *uri = "{uri}";
+ char *clientID = "{clientID}";
+ int rc;
+
+ MQTTClient_createOptions createOpts = MQTTClient_createOptions_initializer;
+
+#ifdef USE_MQTT_5
+ conn_opts.MQTTVersion = MQTTVERSION_5;
+ conn_opts.cleanstart = 1;
+
+ createOpts.MQTTVersion = MQTTVERSION_5;
+#else
+ conn_opts.cleansession = 1;
+#endif
+
+ MQTTClient_setTraceCallback(trace_callback);
+ MQTTClient_setTraceLevel(MQTT_DEBUG_LEVEL);
+
+ rc = MQTTClient_createWithOptions(
+ &client, uri, clientID, MQTTCLIENT_PERSISTENCE_NONE, NULL, &createOpts);
+ if (rc != MQTTCLIENT_SUCCESS)
+ {{
+ LogError("MQTT Failed to create client, return code %d\n", rc);
+ goto exit_error;
+ }}
+
+ rc = MQTTClient_setCallbacks(client, NULL, connectionLost, messageArrived, NULL);
+ if (rc != MQTTCLIENT_SUCCESS)
+ {{
+ LogError("MQTT Failed to set callbacks, return code %d\n", rc);
+ goto exit_error;
+ }}
+
+{init}
+
+ rc = _connect_mqtt();
+ if (rc == MQTTCLIENT_SUCCESS) {{
+ LogInfo("MQTT Connected\n");
+ }} else {{
+ LogError("MQTT Connect Failed, return code %d\n", rc);
+ // Connect error at init is fine, publish thread will retry later
+ }}
+
+ /* start MQTT thread */
+ MQTT_stop_thread = 0;
+ rc = pthread_create(&MQTT_thread, NULL, &__MQTT_thread_proc, NULL);
+ if (rc != 0) {{
+ LogError("MQTT cannot create thread, return code %d\n", rc);
+ goto exit_error;
+ }}
+
+ return 0;
+
+exit_error:
+ MQTTClient_destroy(&client);
+ return rc;
+}}
+
+#define READ_VALUE(c_loc_name, C_type) \
+ if(MQTT_##c_loc_name##_state == CHANGED){{ \
+ /* TODO care about endianess */ \
+ PLC_##c_loc_name##_buf = MQTT_##c_loc_name##_buf; \
+ MQTT_##c_loc_name##_state = UNCHANGED; \
+ }}
+
+void __retrieve_{locstr}(void)
+{{
+ if (pthread_mutex_trylock(&MQTT_retrieve_mutex) == 0){{
+{retrieve}
+ pthread_mutex_unlock(&MQTT_retrieve_mutex);
+ }}
+}}
+
+#define WRITE_VALUE(c_loc_name, C_type) \
+ /* TODO care about endianess */ \
+ if(MQTT_##c_loc_name##_buf != PLC_##c_loc_name##_buf){{ \
+ MQTT_##c_loc_name##_buf = PLC_##c_loc_name##_buf; \
+ MQTT_##c_loc_name##_state = CHANGED; \
+ MQTT_any_pub_var_changed = 1; \
+ }}
+
+void __publish_{locstr}(void)
+{{
+ if (pthread_mutex_trylock(&MQTT_thread_wakeup_mutex) == 0){{
+ MQTT_any_pub_var_changed = 0;
+ /* copy PLC_* variables to MQTT_*, and mark those who changed */
+{publish}
+ /* if any change detcted, unblock publish thread */
+ if(MQTT_any_pub_var_changed || MQTT_is_disconnected){{
+ pthread_cond_signal(&MQTT_thread_wakeup);
+ }}
+ pthread_mutex_unlock(&MQTT_thread_wakeup_mutex);
+ }} else {{
+ /* TODO if couldn't lock mutex set status variable accordingly */
+ }}
+}}
+
--- a/svghmi/svghmi_server.py Tue Aug 20 01:51:08 2024 +0200
+++ b/svghmi/svghmi_server.py Sat Sep 07 12:50:57 2024 +0200
@@ -174,7 +174,6 @@
self.lock = RLock()
self.initial_timeout = initial_timeout
self.interval = interval
- self.callback = callback
with self.lock:
self._start()
@@ -310,9 +309,9 @@
def waitpid_timeout_loop(proc = proc, timeout = timeout):
try:
while proc.poll() is None:
- time.sleep(1)
- timeout = timeout - 1
- if not timeout:
+ time.sleep(.1)
+ timeout = timeout - .1
+ if timeout <= 0:
GetPLCObjectSingleton().LogMessage(
LogLevelsDict["WARNING"],
"Timeout waiting for {} PID: {}".format(helpstr, str(proc.pid)))
@@ -320,5 +319,5 @@
except OSError:
# workaround exception "OSError: [Errno 10] No child processes"
pass
- Thread(target=waitpid_timeout_loop, name="Zombie hunter").start()
-
+ waitpid_timeout_loop()
+
--- a/targets/toolchain_gcc.py Tue Aug 20 01:51:08 2024 +0200
+++ b/targets/toolchain_gcc.py Sat Sep 07 12:50:57 2024 +0200
@@ -38,6 +38,18 @@
includes_re = re.compile(r'\s*#include\s*["<]([^">]*)[">].*')
+def compute_file_md5(filetocheck):
+ hasher = hashlib.md5()
+ with open(filetocheck, 'rb') as afile:
+ while True:
+ buf = afile.read(65536)
+ if len(buf) > 0:
+ hasher.update(buf)
+ else:
+ break
+ return hasher.hexdigest()
+
+
class toolchain_gcc(object):
"""
This abstract class contains GCC specific code.
@@ -136,9 +148,9 @@
# Get latest computed hash and deps
oldhash, deps = self.srcmd5.get(bn, (None, []))
# read source
- src = open(os.path.join(self.buildpath, bn)).read()
+ src = os.path.join(self.buildpath, bn)
# compute new hash
- newhash = hashlib.md5(src.encode()).hexdigest()
+ newhash = compute_file_md5(src)
# compare
match = (oldhash == newhash)
if not match:
@@ -256,7 +268,7 @@
self.CTRInstance.logger.write(" [pass] " + ' '.join(obns)+" -> " + self.bin + "\n")
# Calculate md5 key and get data for the new created PLC
- self.md5key = hashlib.md5(open(self.bin_path, "rb").read()).hexdigest()
+ self.md5key = compute_file_md5(self.bin_path)
# Store new PLC filename based on md5 key
f = open(self._GetMD5FileName(), "w")
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/beremiz.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,5 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="PYRO://localhost:61131" Disable_Extensions="false">
+ <TargetType/>
+ <Libraries Enable_Python_Library="false"/>
+</BeremizRoot>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/mqtt_0@mqtt/baseconfnode.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="0" Name="mqtt_0"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/mqtt_0@mqtt/confnode.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<MQTTClient xmlns:xsd="http://www.w3.org/2001/XMLSchema" Broker_URI="ws://172.17.0.1:1883" Use_MQTT_5="true"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/mqtt_0@mqtt/selected.csv Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,10 @@
+input,smta,1,DINT,0
+input,smtb,1,DINT,1
+input,smtc,1,DINT,2
+input,smtd,1,DINT,3
+input,smte,1,DINT,4
+output,smtf,1,False,DINT,0
+output,smtg,1,False,DINT,1
+output,smth,1,False,DINT,2
+output,smti,1,False,DINT,3
+output,smtj,1,False,DINT,4
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_client/plc.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,296 @@
+<?xml version='1.0' encoding='utf-8'?>
+<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
+ <fileHeader companyName="Beremiz" productName="Beremiz" productVersion="1" creationDateTime="2016-10-24T18:09:22"/>
+ <contentHeader name="First Steps" modificationDateTime="2024-07-25T16:55:27">
+ <coordinateInfo>
+ <fbd>
+ <scaling x="0" y="0"/>
+ </fbd>
+ <ld>
+ <scaling x="0" y="0"/>
+ </ld>
+ <sfc>
+ <scaling x="0" y="0"/>
+ </sfc>
+ </coordinateInfo>
+ </contentHeader>
+ <types>
+ <dataTypes/>
+ <pous>
+ <pou name="plc_prg" pouType="program">
+ <interface>
+ <localVars>
+ <variable name="LocalVar0">
+ <type>
+ <DINT/>
+ </type>
+ <initialValue>
+ <simpleValue value="0"/>
+ </initialValue>
+ </variable>
+ </localVars>
+ <localVars>
+ <variable name="LocalVar1" address="%QD0.2">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ <variable name="LocalVar2" address="%ID0.2">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ </localVars>
+ </interface>
+ <body>
+ <FBD>
+ <inVariable localId="1" executionOrderId="0" height="27" width="82" negated="false">
+ <position x="421" y="83"/>
+ <connectionPointOut>
+ <relPosition x="82" y="13"/>
+ </connectionPointOut>
+ <expression>LocalVar0</expression>
+ </inVariable>
+ <inVariable localId="3" executionOrderId="0" height="27" width="82" negated="false">
+ <position x="126" y="224"/>
+ <connectionPointOut>
+ <relPosition x="82" y="13"/>
+ </connectionPointOut>
+ <expression>LocalVar2</expression>
+ </inVariable>
+ <block localId="4" typeName="ADD" executionOrderId="0" height="60" width="63">
+ <position x="112" y="48"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="2">
+ <position x="112" y="78"/>
+ <position x="102" y="78"/>
+ <position x="102" y="33"/>
+ <position x="314" y="33"/>
+ <position x="314" y="78"/>
+ <position x="291" y="78"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="5">
+ <position x="112" y="98"/>
+ <position x="64" y="98"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="63" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inOutVariable localId="2" executionOrderId="0" height="27" width="82" negatedOut="false" negatedIn="false">
+ <position x="209" y="65"/>
+ <connectionPointIn>
+ <relPosition x="0" y="13"/>
+ <connection refLocalId="4" formalParameter="OUT">
+ <position x="209" y="78"/>
+ <position x="175" y="78"/>
+ </connection>
+ </connectionPointIn>
+ <connectionPointOut>
+ <relPosition x="82" y="13"/>
+ </connectionPointOut>
+ <expression>LocalVar0</expression>
+ </inOutVariable>
+ <inVariable localId="5" executionOrderId="0" height="27" width="18" negated="false">
+ <position x="46" y="85"/>
+ <connectionPointOut>
+ <relPosition x="18" y="13"/>
+ </connectionPointOut>
+ <expression>1</expression>
+ </inVariable>
+ <block localId="6" typeName="MOD" executionOrderId="0" height="60" width="63">
+ <position x="588" y="66"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="1">
+ <position x="588" y="96"/>
+ <position x="503" y="96"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="7">
+ <position x="588" y="116"/>
+ <position x="548" y="116"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="63" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inVariable localId="7" executionOrderId="0" height="27" width="26" negated="false">
+ <position x="522" y="103"/>
+ <connectionPointOut>
+ <relPosition x="26" y="13"/>
+ </connectionPointOut>
+ <expression>50</expression>
+ </inVariable>
+ <block localId="8" typeName="GT" executionOrderId="0" height="60" width="63">
+ <position x="721" y="65"/>
+ <inputVariables>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="6" formalParameter="OUT">
+ <position x="721" y="95"/>
+ <position x="704" y="95"/>
+ <position x="704" y="96"/>
+ <position x="651" y="96"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN2">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="9">
+ <position x="721" y="115"/>
+ <position x="709" y="115"/>
+ <position x="709" y="117"/>
+ <position x="697" y="117"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="63" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inVariable localId="9" executionOrderId="0" height="27" width="26" negated="false">
+ <position x="671" y="104"/>
+ <connectionPointOut>
+ <relPosition x="26" y="13"/>
+ </connectionPointOut>
+ <expression>24</expression>
+ </inVariable>
+ <outVariable localId="10" executionOrderId="0" height="27" width="82" negated="false">
+ <position x="1112" y="106"/>
+ <connectionPointIn>
+ <relPosition x="0" y="13"/>
+ <connection refLocalId="11" formalParameter="OUT">
+ <position x="1112" y="119"/>
+ <position x="1065" y="119"/>
+ <position x="1065" y="83"/>
+ <position x="1019" y="83"/>
+ </connection>
+ </connectionPointIn>
+ <expression>LocalVar1</expression>
+ </outVariable>
+ <block localId="11" typeName="SEL" executionOrderId="0" height="80" width="63">
+ <position x="956" y="53"/>
+ <inputVariables>
+ <variable formalParameter="G">
+ <connectionPointIn>
+ <relPosition x="0" y="30"/>
+ <connection refLocalId="8" formalParameter="OUT">
+ <position x="956" y="83"/>
+ <position x="870" y="83"/>
+ <position x="870" y="95"/>
+ <position x="784" y="95"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN0">
+ <connectionPointIn>
+ <relPosition x="0" y="50"/>
+ <connection refLocalId="12">
+ <position x="956" y="103"/>
+ <position x="927" y="103"/>
+ <position x="927" y="115"/>
+ <position x="898" y="115"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ <variable formalParameter="IN1">
+ <connectionPointIn>
+ <relPosition x="0" y="70"/>
+ <connection refLocalId="13">
+ <position x="956" y="123"/>
+ <position x="933" y="123"/>
+ <position x="933" y="171"/>
+ <position x="910" y="171"/>
+ </connection>
+ </connectionPointIn>
+ </variable>
+ </inputVariables>
+ <inOutVariables/>
+ <outputVariables>
+ <variable formalParameter="OUT">
+ <connectionPointOut>
+ <relPosition x="63" y="30"/>
+ </connectionPointOut>
+ </variable>
+ </outputVariables>
+ </block>
+ <inVariable localId="12" executionOrderId="0" height="27" width="34" negated="false">
+ <position x="872" y="102"/>
+ <connectionPointOut>
+ <relPosition x="34" y="13"/>
+ </connectionPointOut>
+ <expression>666</expression>
+ </inVariable>
+ <inVariable localId="13" executionOrderId="0" height="27" width="34" negated="false">
+ <position x="876" y="158"/>
+ <connectionPointOut>
+ <relPosition x="34" y="13"/>
+ </connectionPointOut>
+ <expression>666</expression>
+ </inVariable>
+ </FBD>
+ </body>
+ </pou>
+ </pous>
+ </types>
+ <instances>
+ <configurations>
+ <configuration name="config">
+ <resource name="resource1">
+ <task name="plc_task" priority="1" interval="T#100ms">
+ <pouInstance name="plc_task_instance" typeName="plc_prg"/>
+ </task>
+ </resource>
+ <globalVars constant="true">
+ <variable name="ResetCounterValue">
+ <type>
+ <INT/>
+ </type>
+ <initialValue>
+ <simpleValue value="17"/>
+ </initialValue>
+ </variable>
+ </globalVars>
+ </configuration>
+ </configurations>
+ </instances>
+</project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/beremiz.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,7 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BeremizRoot xmlns:xsd="http://www.w3.org/2001/XMLSchema" URI_location="PYRO://192.168.45.1:3000">
+ <TargetType>
+ <LHC2_GOT_012/>
+ </TargetType>
+ <Libraries Enable_SVGHMI_Library="false"/>
+</BeremizRoot>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/mqtt_0@mqtt/baseconfnode.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,2 @@
+<?xml version='1.0' encoding='utf-8'?>
+<BaseParams xmlns:xsd="http://www.w3.org/2001/XMLSchema" IEC_Channel="3" Name="mqtt_0"/>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/mqtt_0@mqtt/confnode.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,6 @@
+<?xml version='1.0' encoding='utf-8'?>
+<MQTTClient xmlns:xsd="http://www.w3.org/2001/XMLSchema" Broker_URI="wss://192.168.19.25:18885">
+ <AuthType>
+ <x509 Client_certificate="" Broker_certificate="test-root-ca.crt" Verify_hostname="false"/>
+ </AuthType>
+</MQTTClient>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/mqtt_0@mqtt/selected.csv Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,2 @@
+input,in_topic1,1,DINT,0
+output,out_topic1,1,False,DINT,0
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/plc.xml Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,107 @@
+<?xml version='1.0' encoding='utf-8'?>
+<project xmlns:ns1="http://www.plcopen.org/xml/tc6_0201" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://www.plcopen.org/xml/tc6_0201">
+ <fileHeader companyName="SMARTEH d.o.o." productName="LHC2_GOT_012" productVersion="1.1.359" creationDateTime="2024-07-25T15:43:22"/>
+ <contentHeader name="mqtt" modificationDateTime="2024-08-21T17:05:30">
+ <coordinateInfo>
+ <fbd>
+ <scaling x="0" y="0"/>
+ </fbd>
+ <ld>
+ <scaling x="0" y="0"/>
+ </ld>
+ <sfc>
+ <scaling x="0" y="0"/>
+ </sfc>
+ </coordinateInfo>
+ </contentHeader>
+ <types>
+ <dataTypes/>
+ <pous>
+ <pou name="program0" pouType="program">
+ <interface>
+ <localVars>
+ <variable name="LocalVar0" address="%ID3.0">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ </localVars>
+ <localVars>
+ <variable name="LocalVar1">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ </localVars>
+ <localVars>
+ <variable name="LocalVar2" address="%QD3.0">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ </localVars>
+ <localVars>
+ <variable name="LocalVar3">
+ <type>
+ <DINT/>
+ </type>
+ </variable>
+ </localVars>
+ </interface>
+ <body>
+ <FBD>
+ <inVariable localId="1" executionOrderId="0" height="26" width="82" negated="false">
+ <position x="173" y="47"/>
+ <connectionPointOut>
+ <relPosition x="82" y="13"/>
+ </connectionPointOut>
+ <expression>LocalVar0</expression>
+ </inVariable>
+ <outVariable localId="2" executionOrderId="0" height="26" width="82" negated="false">
+ <position x="482" y="47"/>
+ <connectionPointIn>
+ <relPosition x="0" y="13"/>
+ <connection refLocalId="1">
+ <position x="482" y="60"/>
+ <position x="255" y="60"/>
+ </connection>
+ </connectionPointIn>
+ <expression>LocalVar1</expression>
+ </outVariable>
+ <outVariable localId="3" executionOrderId="0" height="26" width="82" negated="false">
+ <position x="479" y="140"/>
+ <connectionPointIn>
+ <relPosition x="0" y="13"/>
+ <connection refLocalId="4">
+ <position x="479" y="153"/>
+ <position x="365" y="153"/>
+ <position x="365" y="168"/>
+ <position x="252" y="168"/>
+ </connection>
+ </connectionPointIn>
+ <expression>LocalVar2</expression>
+ </outVariable>
+ <inVariable localId="4" executionOrderId="0" height="26" width="82" negated="false">
+ <position x="170" y="155"/>
+ <connectionPointOut>
+ <relPosition x="82" y="13"/>
+ </connectionPointOut>
+ <expression>LocalVar3</expression>
+ </inVariable>
+ </FBD>
+ </body>
+ </pou>
+ </pous>
+ </types>
+ <instances>
+ <configurations>
+ <configuration name="config">
+ <resource name="resource1">
+ <task name="main_task" priority="0" interval="T#1000ms">
+ <pouInstance name="instance0" typeName="program0"/>
+ </task>
+ </resource>
+ </configuration>
+ </configurations>
+ </instances>
+</project>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/projects/mqtt_ssl/project_files/test-root-ca.crt Sat Sep 07 12:50:57 2024 +0200
@@ -0,0 +1,23 @@
+-----BEGIN CERTIFICATE-----
+MIIDwDCCAqigAwIBAgIUPajsILLwblnD7WEDCk00ebOAASgwDQYJKoZIhvcNAQEL
+BQAwcDELMAkGA1UEBhMCR0IxEjAQBgNVBAgMCVdpbHRzaGlyZTESMBAGA1UEBwwJ
+U2FsaXNidXJ5MRUwEwYDVQQKDAxQYWhvIFByb2plY3QxEDAOBgNVBAsMB1Rlc3Rp
+bmcxEDAOBgNVBAMMB1Jvb3QgQ0EwIBcNMjMwNDEzMTMyMjMxWhgPMjEwNTA2MDIx
+MzIyMzFaMHAxCzAJBgNVBAYTAkdCMRIwEAYDVQQIDAlXaWx0c2hpcmUxEjAQBgNV
+BAcMCVNhbGlzYnVyeTEVMBMGA1UECgwMUGFobyBQcm9qZWN0MRAwDgYDVQQLDAdU
+ZXN0aW5nMRAwDgYDVQQDDAdSb290IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
+MIIBCgKCAQEAsqFZCPniXeWNtd5NhccCmirdLv6i/c6FAffg4iMPEYanDTh0PPWE
+/tPiiOTzcGp5udF3hjjtv2lx5OSEaZYNSIgWERf0LPS0MBsHjMmVoU4S8+gZAst1
+igzWKmmZAaUwBdGz0D6cRr7ZMUBfzJnrF1V6VQMLNXfrXmYAXRpxRMcBv3FPYLUc
+qXp9gbFYnc+xNbuDE3Ir6/wMOu3Q1H6BvnIGVmdGqnmrONqzkMcqCuTEGlFEBQ4s
+HrZUbXQGSKtlMInAM1hyP+qHmuMwTfue8/qspewHds9fNstJL9wLCZBk3Jdf6B5I
+S6S+cmHM00cThnuVVwnwgU6syGhgekCNYwIDAQABo1AwTjAdBgNVHQ4EFgQU/piK
+w0GycTpKs3ZaPBcGkxGZjsgwHwYDVR0jBBgwFoAU/piKw0GycTpKs3ZaPBcGkxGZ
+jsgwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAhno5Byj3BGy/je/u
+yPl8/uGOqh+uK0Ds+6YvUBRTHlnAFegwQrJhq17bzTdZZlw5s0+bYEVtOwX6p7Zk
+mT4zUEPEQ7B223+r3TiB5xQ4ad+wZsDa9X7XY0+vqc+wf4CHFPHX4ZR0Xa1p/vBL
+Y3L6ANHkwRFgx+XREmkiv4GJxeZtNSevuM63bbRZ/Y7OSZQlvKTxNlsKqi+61d7w
+ciWk9BmmXNm2kQeKwsx5o7bMiETOhDXobcibD8R3U+3tf3vWp9jWIMZKkJ40U1Wb
+LM0S9B1ZBkqb3Ml4Rq0AhQntQNGKDobt93ilQoGSUKq0/bR5wA8Yad9viVbbbSsm
+kkdThg==
+-----END CERTIFICATE-----
--- a/util/paths.py Tue Aug 20 01:51:08 2024 +0200
+++ b/util/paths.py Sat Sep 07 12:50:57 2024 +0200
@@ -46,11 +46,11 @@
path = os.path.dirname(path)
return path
-def ThirdPartyPath(name):
+def ThirdPartyPath(name, *suffixes):
"""
Return folder where to find sibling projects like Modbus, CanFestival, BACnet
"""
- return os.path.join(AbsParentDir(__file__, 2), name)
+ return os.path.join(AbsParentDir(__file__, 2), name, *suffixes)
def Bpath(*names):
"""