merge MQTT grafted from python2 branch - untested
authorEdouard Tisserant <edouard.tisserant@gmail.com>
Sat, 07 Sep 2024 12:50:57 +0200
changeset 4007 76dede1e3403
parent 4006 e16c8443e877 (diff)
parent 3978 22ba103801ee (current diff)
child 4008 f30573e98600
merge MQTT grafted from python2 branch - untested
--- 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):
     """