WIP: Add skeleton for MQTT extension based on part of OPC-UA extension.
authorEdouard Tisserant <edouard@beremiz.fr>
Wed, 12 Jun 2024 11:45:09 +0200
changeset 3979 76295adcf940
parent 3960 9271afc4f34a
child 3980 96acfee19caf
WIP: Add skeleton for MQTT extension based on part of OPC-UA extension.

For now generated code makes no sense but persistently-configurable-C-generating-extension infrastructure is there.
features.py
mqtt/__init__.py
mqtt/client.py
mqtt/mqtt_client_gen.py
--- a/features.py	Wed Jun 05 15:18:15 2024 +0200
+++ b/features.py	Wed Jun 12 11:45:09 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	Wed Jun 12 11:45:09 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	Wed Jun 12 11:45:09 2024 +0200
@@ -0,0 +1,145 @@
+# mqtt/client.py
+
+from __future__ import absolute_import
+
+import os
+
+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
+
+PahoMqttCPath = paths.ThirdPartyPath("MQTT")
+PahoMqttCLibraryPath = PahoMqttCPath 
+PahoMqttCIncludePaths = [PahoMqttCPath]
+
+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="Certificate" type="xsd:string" use="optional" default="certificate.pem"/>
+                      <xsd:attribute name="PrivateKey" type="xsd:string" use="optional" default="private_key.pem"/>
+                    </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="Broker_URI" type="xsd:string" use="optional" default="ws://localhost:1883"/>
+        </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)
+
+        paramList = authParams.get(AuthType, None)
+        if paramList:
+            for name,default in paramList:
+                value = cfg("AuthType."+name)
+                if value == "" or value is None:
+                    value = default
+                # cryptomaterial is expected to be in project's user provide file directory
+                if name in ["Certificate","PrivateKey"]:
+                    value = os.path.join(self.GetCTRoot()._getProjectFilesPath(), 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 "beremiz.h"\n'
+        c_code += self.modeldata.GenerateC(c_path, locstr, self.GetConfig())
+
+        with open(c_path, 'w') as c_file:
+            c_file.write(c_code)
+
+        LDFLAGS = [' "' + os.path.join(PahoMqttCLibraryPath, "libpaho-mqtt3as.a") + '"', '-lcrypto']
+
+        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 = []
+        for direction, data in self.modeldata.iteritems():
+            iec_direction_prefix = {"input": "__I", "output": "__Q"}[direction]
+            for row in data:
+                dname, ua_nsidx, ua_nodeid_type, _ua_node_id, ua_type, iec_number = row
+                iec_type, C_type, iec_size_prefix, ua_type_enum, ua_type = MQTT_IEC_types[ua_type]
+                c_loc_name = iec_direction_prefix + iec_size_prefix + locstr + "_" + str(iec_number)
+                entries.append({
+                    "name": dname,
+                    "type": {"input": LOCATION_VAR_INPUT, "output": LOCATION_VAR_OUTPUT}[direction],
+                    "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_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": entries}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mqtt/mqtt_client_gen.py	Wed Jun 12 11:45:09 2024 +0200
@@ -0,0 +1,639 @@
+from __future__ import print_function
+from __future__ import absolute_import
+
+import csv
+import functools
+from threading import Thread
+
+import wx
+import wx.dataview as dv
+
+
+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]
+
+lstcolnames  = [ "Topic",  "QoS", "Type", "Location"]
+lstcolwidths = [     100,     50,    100,         50]
+lstcoltypess = [     str,    int,    str,        int]
+lstcoldeflts = [ "a/b/c",    "1", "DINT",        "0"]
+Location_column = lstcolnames.index("Location")
+
+directions = ["input", "output"]
+
+authParams = {
+    "x509":[
+        ("Certificate", "certificate.der"),
+        ("PrivateKey", "private_key.pem")],
+    "UserPassword":[
+        ("User", None),
+        ("Password", None)]}
+
+class MQTTSubListModel(dv.PyDataViewIndexListModel):
+    def __init__(self, data, log):
+        dv.PyDataViewIndexListModel.__init__(self, len(data))
+        self.data = data
+        self.log = log
+
+    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 = lstcoltypess[col]
+
+        try:
+            v = expectedtype(value)
+        except ValueError: 
+            self.log("String {} is invalid for type {}\n".format(value,expectedtype.__name__))
+            return False
+
+        if col == lstcolnames.index("QoS") and v not in QoS_values:
+            self.log("{} is invalid for IdType\n".format(value))
+            return False
+
+        self.data[row][col] = v
+        return True
+
+    # Report how many columns this model provides data for.
+    def GetColumnCount(self):
+        return len(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 == 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, lstcoldeflts[:])
+        # notify views
+        self.RowInserted(row)
+    
+    def ResetData(self):
+        self.Reset(len(self.data))
+
+class MQTTSubListPanel(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)
+
+        for idx,(colname,width) in enumerate(zip(lstcolnames,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.Panel):
+    def __init__(self, parent, modeldata, log, config_getter):
+        self.log = log
+        wx.Panel.__init__(self, parent)
+
+        # TODO replace FlexGridSizer with a simpler one
+        self.inout_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
+        self.inout_sizer.AddGrowableCol(0)
+        self.inout_sizer.AddGrowableRow(0)
+
+        self.config_getter = config_getter
+
+        self.selected_splitter = wx.SplitterWindow(self, style=wx.SUNKEN_BORDER | wx.SP_3D)
+
+        self.selected_datas = modeldata
+        self.selected_models = { direction:MQTTSubListModel(self.selected_datas[direction], log) for direction in directions }
+        self.selected_lists = { direction:MQTTSubListPanel(
+                self.selected_splitter, log, 
+                self.selected_models[direction], direction) 
+            for direction in directions }
+
+        self.selected_splitter.SplitHorizontally(*[self.selected_lists[direction] for direction in directions]+[300])
+
+        self.inout_sizer.Add(self.selected_splitter, flag=wx.GROW)
+        self.inout_sizer.Layout()
+        self.SetAutoLayout(True)
+        self.SetSizer(self.inout_sizer)
+
+    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):
+        super(MQTTClientList, self).__init__(self)
+        self.log = log
+        self.change_callback = change_callback
+
+    def append(self, value):
+        v = dict(list(zip(lstcolnames, value)))
+
+        if type(v["Location"]) != int:
+            if len(self) == 0:
+                v["Location"] = 0
+            else:
+                iecnums = set(zip(*self)[Location_column])
+                greatest = max(iecnums)
+                holes = set(range(greatest)) - iecnums
+                v["Location"] = min(holes) if holes else greatest+1
+
+        if v["QoS"] not in QoS_values:
+            self.log("Unknown QoS\n".format(value))
+            return False
+
+        try:
+            for t,n in zip(lstcoltypess, lstcolnames):
+                v[n] = t(v[n]) 
+        except ValueError: 
+            self.log("MQTT topic {} (Location={}) has invalid type\n".format(v["Topic"],v["Location"]))
+            return False
+
+        if len(self)>0 and v["Topic"] in list(zip(*self))[lstcolnames.index("Topic")]:
+            self.log("MQTT topic {} (Location={}) already in the list\n".format(v["Topic"],v["Location"]))
+            return False
+
+        list.append(self, [v[n] for n in lstcolnames])
+
+        self.change_callback()
+
+        return True
+
+    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)
+
+    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
+                list.append(self[direction],row[1:])
+
+    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):
+        template = """/* code generated by beremiz MQTT extension */
+
+#include <open62541/client_config_default.h>
+#include <open62541/client_highlevel.h>
+#include <open62541/plugin/log_stdout.h>
+#include <open62541/plugin/securitypolicy.h>
+#include <open62541/plugin/securitypolicy_default.h>
+
+#include <open62541/types.h>
+#include <open62541/types_generated_handling.h>
+
+#define _Log(level, ...)                                                                           \\
+    {{                                                                                             \\
+        char mstr[256];                                                                            \\
+        snprintf(mstr, 255, __VA_ARGS__);                                                          \\
+        LogMessage(level, mstr, strlen(mstr));                                                     \\
+    }}
+
+#define LogInfo(...) _Log(LOG_INFO, __VA_ARGS__);
+#define LogError(...) _Log(LOG_CRITICAL, __VA_ARGS__);
+#define LogWarning(...) _Log(LOG_WARNING, __VA_ARGS__);
+
+static MQTT_INLINE MQTT_ByteString
+loadFile(const char *const path) {{
+    MQTT_ByteString fileContents = MQTT_STRING_NULL;
+
+    FILE *fp = fopen(path, "rb");
+    if(!fp) {{
+        errno = 0;
+        LogError("OPC-MQTT could not open %s", path);
+        return fileContents;
+    }}
+
+    fseek(fp, 0, SEEK_END);
+    fileContents.length = (size_t)ftell(fp);
+    fileContents.data = (MQTT_Byte *)MQTT_malloc(fileContents.length * sizeof(MQTT_Byte));
+    if(fileContents.data) {{
+        fseek(fp, 0, SEEK_SET);
+        size_t read = fread(fileContents.data, sizeof(MQTT_Byte), fileContents.length, fp);
+        if(read != fileContents.length){{
+            MQTT_ByteString_clear(&fileContents);
+            LogError("OPC-MQTT could not read %s", path);
+        }}
+    }} else {{
+        fileContents.length = 0;
+        LogError("OPC-MQTT Not enough memoty to load %s", path);
+    }}
+    fclose(fp);
+
+    return fileContents;
+}}
+
+static MQTT_Client *client;
+static MQTT_ClientConfig *cc;
+
+#define DECL_VAR(ua_type, C_type, c_loc_name)                                                       \\
+static MQTT_Variant c_loc_name##_variant;                                                             \\
+static C_type c_loc_name##_buf = 0;                                                                 \\
+C_type *c_loc_name = &c_loc_name##_buf;
+
+{decl}
+
+void __cleanup_{locstr}(void)
+{{
+    MQTT_Client_disconnect(client);
+    MQTT_Client_delete(client);
+}}
+
+#define INIT_NoAuth()                                                                              \\
+    LogInfo("OPC-MQTT Init no auth");                                                                \\
+    MQTT_ClientConfig_setDefault(cc);                                                                \\
+    retval = MQTT_Client_connect(client, uri);
+
+/* Note : Single policy is enforced here, by default open62541 client supports all policies */
+#define INIT_x509(Policy, UpperCaseMode, PrivateKey, Certificate)                                  \\
+    LogInfo("OPC-MQTT Init x509 %s,%s,%s,%s", #Policy, #UpperCaseMode, PrivateKey, Certificate);     \\
+                                                                                                   \\
+    MQTT_ByteString certificate = loadFile(Certificate);                                             \\
+    MQTT_ByteString privateKey  = loadFile(PrivateKey);                                              \\
+                                                                                                   \\
+    cc->securityMode = MQTT_MESSAGESECURITYMODE_##UpperCaseMode;                                     \\
+                                                                                                   \\
+    /* replacement for default behaviour */                                                        \\
+    /* MQTT_ClientConfig_setDefaultEncryption(cc, certificate, privateKey, NULL, 0, NULL, 0); */     \\
+    do{{                                                                                           \\
+        retval = MQTT_ClientConfig_setDefault(cc);                                                   \\
+        if(retval != MQTT_STATUSCODE_GOOD)                                                           \\
+            break;                                                                                 \\
+                                                                                                   \\
+        MQTT_SecurityPolicy *sp = (MQTT_SecurityPolicy*)                                               \\
+            MQTT_realloc(cc->securityPolicies, sizeof(MQTT_SecurityPolicy) * 2);                       \\
+        if(!sp){{                                                                                  \\
+            retval = MQTT_STATUSCODE_BADOUTOFMEMORY;                                                 \\
+            break;                                                                                 \\
+        }}                                                                                         \\
+        cc->securityPolicies = sp;                                                                 \\
+                                                                                                   \\
+        retval = MQTT_SecurityPolicy_##Policy(&cc->securityPolicies[cc->securityPoliciesSize],       \\
+                                                 certificate, privateKey, &cc->logger);            \\
+        if(retval != MQTT_STATUSCODE_GOOD) {{                                                        \\
+            MQTT_LOG_WARNING(&cc->logger, MQTT_LOGCATEGORY_USERLAND,                                   \\
+                           "Could not add SecurityPolicy Policy with error code %s",               \\
+                           MQTT_StatusCode_name(retval));                                            \\
+            MQTT_free(cc->securityPolicies);                                                         \\
+            cc->securityPolicies = NULL;                                                           \\
+            break;                                                                                 \\
+        }}                                                                                         \\
+                                                                                                   \\
+        ++cc->securityPoliciesSize;                                                                \\
+    }} while(0);                                                                                   \\
+                                                                                                   \\
+    retval = MQTT_Client_connect(client, uri);                                                       \\
+                                                                                                   \\
+    MQTT_ByteString_clear(&certificate);                                                             \\
+    MQTT_ByteString_clear(&privateKey);
+
+#define INIT_UserPassword(User, Password)                                                          \\
+    LogInfo("OPC-MQTT Init UserPassword %s,%s", User, Password);                                     \\
+    MQTT_ClientConfig_setDefault(cc);                                                                \\
+    retval = MQTT_Client_connectUsername(client, uri, User, Password);
+
+#define INIT_READ_VARIANT(ua_type, c_loc_name)                                                     \\
+    MQTT_Variant_init(&c_loc_name##_variant);
+
+#define INIT_WRITE_VARIANT(ua_type, ua_type_enum, c_loc_name)                                      \\
+    MQTT_Variant_setScalar(&c_loc_name##_variant, (ua_type*)c_loc_name, &MQTT_TYPES[ua_type_enum]);
+
+int __init_{locstr}(int argc,char **argv)
+{{
+    MQTT_StatusCode retval;
+    client = MQTT_Client_new();
+    cc = MQTT_Client_getConfig(client);
+    char *uri = "{uri}";
+{init}
+
+    if(retval != MQTT_STATUSCODE_GOOD) {{
+        LogError("OPC-MQTT Init Failed %d", retval);
+        MQTT_Client_delete(client);
+        return EXIT_FAILURE;
+    }}
+    return 0;
+}}
+
+#define READ_VALUE(ua_type, ua_type_enum, c_loc_name, ua_nodeid_type, ua_nsidx, ua_node_id)        \\
+    retval = MQTT_Client_readValueAttribute(                                                         \\
+        client, ua_nodeid_type(ua_nsidx, ua_node_id), &c_loc_name##_variant);                      \\
+    if(retval == MQTT_STATUSCODE_GOOD && MQTT_Variant_isScalar(&c_loc_name##_variant) &&               \\
+       c_loc_name##_variant.type == &MQTT_TYPES[ua_type_enum]) {{                                    \\
+            c_loc_name##_buf = *(ua_type*)c_loc_name##_variant.data;                               \\
+            MQTT_Variant_clear(&c_loc_name##_variant);  /* Unalloc requiered on each read ! */       \\
+    }}
+
+void __retrieve_{locstr}(void)
+{{
+    MQTT_StatusCode retval;
+{retrieve}
+}}
+
+#define WRITE_VALUE(ua_type, c_loc_name, ua_nodeid_type, ua_nsidx, ua_node_id)                     \\
+    MQTT_Client_writeValueAttribute(                                                                 \\
+        client, ua_nodeid_type(ua_nsidx, ua_node_id), &c_loc_name##_variant);
+
+void __publish_{locstr}(void)
+{{
+{publish}
+}}
+
+"""
+        
+        formatdict = dict(
+            locstr   = locstr,
+            uri      = config["URI"],
+            decl     = "",
+            cleanup  = "",
+            init     = "",
+            retrieve = "",
+            publish  = "" 
+        )
+
+        AuthType = config["AuthType"]
+        if AuthType == "x509":
+            formatdict["init"] += """
+    INIT_x509("{PrivateKey}", "{Certificate}")""".format(**config)
+        elif AuthType == "UserPassword":
+            formatdict["init"] += """
+    INIT_UserPassword("{User}", "{Password}")""".format(**config)
+        else:
+            formatdict["init"] += """
+    INIT_NoAuth()"""
+
+        for direction, data in self.items():
+            iec_direction_prefix = {"input": "__I", "output": "__Q"}[direction]
+#            for row in data:
+#                name, ua_nsidx, ua_nodeid_type, _ua_node_id, ua_type, iec_number = row
+#                iec_type, C_type, iec_size_prefix, ua_type_enum, ua_type = MQTT_IEC_types[ua_type]
+#                c_loc_name = iec_direction_prefix + iec_size_prefix + locstr + "_" + str(iec_number)
+#                ua_nodeid_type, id_formating = MQTT_NODE_ID_types[ua_nodeid_type]
+#                ua_node_id = id_formating.format(_ua_node_id)
+#
+#                formatdict["decl"] += """
+#DECL_VAR({ua_type}, {C_type}, {c_loc_name})""".format(**locals())
+#
+#                if direction == "input":
+#                    formatdict["init"] += """
+#    INIT_READ_VARIANT({ua_type}, {c_loc_name})""".format(**locals())
+#                    formatdict["retrieve"] += """
+#    READ_VALUE({ua_type}, {ua_type_enum}, {c_loc_name}, {ua_nodeid_type}, {ua_nsidx}, {ua_node_id})""".format(**locals())
+#
+#                if direction == "output":
+#                    formatdict["init"] += """
+#    INIT_WRITE_VARIANT({ua_type}, {ua_type_enum}, {c_loc_name})""".format(**locals())
+#                    formatdict["publish"] += """
+#    WRITE_VALUE({ua_type}, {c_loc_name}, {ua_nodeid_type}, {ua_nsidx}, {ua_node_id})""".format(**locals())
+
+        Ccode = 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 "opc.tcp://localhost:4840"
+    config["AuthType"] = None
+
+    if argc > 2:
+        AuthType = sys.argv[2]
+        config["AuthType"] = AuthType
+        for (name, default), value in zip_longest(authParams[AuthType], sys.argv[3:]):
+            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()