mqtt/mqtt_client_gen.py
author Edouard Tisserant <edouard@beremiz.fr>
Thu, 18 Jul 2024 11:42:08 +0200
changeset 3992 056657cd1484
parent 3990 24656e0e8732
child 3993 78f9abfb32a0
permissions -rw-r--r--
MQTT: Fix overkill use of wx sizer
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

# 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"]

authParams = {
    "x509":[
        ("Certificate", "certificate.der"),
        ("PrivateKey", "private_key.pem")],
    "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

        self.data[row][col] = v
        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 append(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

        if v["QoS"] not in QoS_values:
            self.log("Unknown QoS\n".format(value))
            return False

        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 False

        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 False

        list.append(self, [v[n] for n in self.dsc.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, 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
                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 <stdint.h>
#include <pthread.h>
#include <string.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__);                                                                      \\
    }}

#define LogInfo(...) _Log(LOG_INFO, __VA_ARGS__);
#define LogError(...) _Log(LOG_CRITICAL, __VA_ARGS__);
#define LogWarning(...) _Log(LOG_WARNING, __VA_ARGS__);

void trace_callback(enum MQTTCLIENT_TRACE_LEVELS level, char* message)
{{
    LogInfo("Paho MQTT Trace : %d, %s\\n", level, 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

/* condition to quit publish thread */
static int MQTT_stop_thread = 0;

/* condition to wakeup publish thread */
static int MQTT_any_pub_var_changed = 0;

/* mutex to keep PLC data consistent, and protect MQTT_any_pub_var_changed */
static pthread_mutex_t MQTT_mutex;

/* wakeup publish thread when PLC changed published variable */
static pthread_cond_t MQTT_new_data = PTHREAD_COND_INITIALIZER;

/* publish thread */
static pthread_t publishThread;

#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}
}};

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

    return rc;
}}

void __cleanup_{locstr}(void)
{{
    int rc;

    /* TODO stop publish thread */

#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)
{{
    LogWarning("ConnectionLost, reconnecting\\n");
    _connect_mqtt();
    /* TODO wait if error */
}}

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){{
        memcpy(topics[mid].mqtt_pdata, (char*)message->payload, message->payloadlen);
        *topics[mid].mqtt_pchanged = 1;
    }} 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(PrivateKey, Certificate)                                                        \\
    LogInfo("MQTT Init x509 %s,%s\\n", PrivateKey, Certificate);
    /* TODO */

#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 void *__publish_thread(void *_unused) {{
    int rc = 0;
    while((rc = pthread_mutex_lock(&MQTT_mutex)) == 0 && !MQTT_stop_thread){{
        pthread_cond_wait(&MQTT_new_data, &MQTT_mutex);
        if(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_mutex);
    }}

    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(MQTTCLIENT_TRACE_ERROR);


    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);
        return rc;
    }}

    rc = MQTTClient_setCallbacks(client, NULL, connectionLost, messageArrived, NULL);
    if (rc != MQTTCLIENT_SUCCESS)
    {{
        LogError("MQTT Failed to set callbacks, return code %d\\n", rc);
        return rc;
    }}

    rc = _connect_mqtt();

    if (rc != MQTTCLIENT_SUCCESS) {{
        LogError("MQTT Connect Failed, return code %d\\n", rc);
        return rc;
    }}

{init}

    /* TODO start publish thread */
    rc = pthread_create(&publishThread, NULL, &__publish_thread, NULL);

    return 0;
}}

#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_mutex) == 0){{
{retrieve}
        pthread_mutex_unlock(&MQTT_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_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){{
            pthread_cond_signal(&MQTT_new_data);
        }}
        pthread_mutex_unlock(&MQTT_mutex);
    }} else {{
        /* TODO if couldn't lock mutex set status variable accordingly */ 
    }}
}}

"""

        formatdict = dict(
            locstr          = locstr,
            uri             = config["URI"],
            clientID        = config["clientID"],
            decl            = "",
            topics          = "",
            cleanup         = "",
            init            = "",
            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"]
        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 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"] += """
    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"] += """
    INIT_SUBSCRIPTION({Topic}, {QoS})""".format(**locals())
            formatdict["retrieve"] += """
        READ_VALUE({c_loc_name}, {C_type})""".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 "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()