opc_ua/opcua_client_maker.py
author Edouard Tisserant <edouard@beremiz.fr>
Mon, 22 Jul 2024 16:13:27 +0200
changeset 3996 4eb23bb4bc2f
parent 3820 46f3ca3f0157
permissions -rw-r--r--
MQTT: Implements reconnecting in publish thread in case thread is waken-up but client is disconnected.

Note: paho's lostConnection callback got already disabled in previous commit.



import csv
import asyncio
import functools
from threading import Thread

from asyncua import Client
from asyncua import ua

import wx
import wx.lib.gizmos as gizmos  # Formerly wx.gizmos in Classic
import wx.dataview as dv


UA_IEC_types = dict(
#   pyopcua | IEC61131|  C  type  | sz |  open62541  enum  | open62541
    Boolean = ("BOOL" , "uint8_t" , "X", "UA_TYPES_BOOLEAN", "UA_Boolean"),
    SByte   = ("SINT" , "int8_t"  , "B", "UA_TYPES_SBYTE"  , "UA_SByte"  ),
    Byte    = ("USINT", "uint8_t" , "B", "UA_TYPES_BYTE"   , "UA_Byte"   ),
    Int16   = ("INT"  , "int16_t" , "W", "UA_TYPES_INT16"  , "UA_Int16"  ),
    UInt16  = ("UINT" , "uint16_t", "W", "UA_TYPES_UINT16" , "UA_UInt16" ),
    Int32   = ("DINT" , "uint32_t", "D", "UA_TYPES_INT32"  , "UA_Int32"  ),
    UInt32  = ("UDINT", "int32_t" , "D", "UA_TYPES_UINT32" , "UA_UInt32" ),
    Int64   = ("LINT" , "int64_t" , "L", "UA_TYPES_INT64"  , "UA_Int64"  ),
    UInt64  = ("ULINT", "uint64_t", "L", "UA_TYPES_UINT64" , "UA_UInt64" ),
    Float   = ("REAL" , "float"   , "D", "UA_TYPES_FLOAT"  , "UA_Float"  ),
    Double  = ("LREAL", "double"  , "L", "UA_TYPES_DOUBLE" , "UA_Double" ),
)

UA_NODE_ID_types = {
    "int"   : ("UA_NODEID_NUMERIC", "{}"  ),
    "str"   : ("UA_NODEID_STRING" , '"{}"'),
    "UUID"  : ("UA_NODEID_UUID"   , '"{}"'),
}

lstcolnames  = [  "Name", "NSIdx", "IdType", "Id", "Type", "IEC"]
lstcolwidths = [     100,      50,      100,  100,    100,    50]
lstcoltypess = [     str,     int,      str,  str,    str,   int]

directions = ["input", "output"]

authParams = {
    "x509":[
        ("Certificate", "certificate.der"),
        ("PrivateKey", "private_key.pem"),
        ("Policy", "Basic256Sha256"),
        ("Mode", "SignAndEncrypt")],
    "UserPassword":[
        ("User", None),
        ("Password", None)]}

class OPCUASubListModel(dv.DataViewIndexListModel):
    def __init__(self, data, log):
        dv.DataViewIndexListModel.__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("IdType") and v not in UA_NODE_ID_types:
            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 == 5:
            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 ResetData(self):
        self.Reset(len(self.data))

OPCUAClientDndMagicWord = "text/beremiz-opcuaclient"

class NodeDropTarget(wx.DropTarget):

    def __init__(self, parent):
        data = wx.CustomDataObject(OPCUAClientDndMagicWord)
        wx.DropTarget.__init__(self, data)
        self.ParentWindow = parent

    def OnDrop(self, x, y):
        self.ParentWindow.OnNodeDnD()
        return True

class OPCUASubListPanel(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)

        DropTarget = NodeDropTarget(self)
        self.SetDropTarget(DropTarget)

        self.Sizer = wx.BoxSizer(wx.VERTICAL)

        self.direction =  direction
        titlestr = direction + " variables"

        title = wx.StaticText(self, label = titlestr)

        delbt = wx.Button(self, label="Delete Row(s)")
        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(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 OnDeleteRows(self, evt):
        items = self.dvc.GetSelections()
        rows = [self.model.GetRow(item) for item in items]
        self.model.DeleteRows(rows)


    def OnNodeDnD(self):
        # Have to find OPC-UA client extension panel from here 
        # in order to avoid keeping reference (otherwise __del__ isn't called)
        #             splitter.        panel.      splitter
        ClientPanel = self.GetParent().GetParent().GetParent()
        nodes = ClientPanel.GetSelectedNodes()
        for node, properties in nodes:
            if properties.cname != "Variable":
                self.log("Node {} ignored (not a variable)".format(properties.dname))
                continue

            tname = properties.variant_type
            if tname not in UA_IEC_types:
                self.log("Node {} ignored (unsupported type)".format(properties.dname))
                continue

            if {"input":ua.AccessLevel.CurrentRead,
                "output":ua.AccessLevel.CurrentWrite}[self.direction] not in properties.access:
                self.log("Node {} ignored because of insuficient access rights".format(properties.dname))
                continue

            nid_type =  type(properties.nid).__name__
            iecid = properties.nid

            value = [properties.dname,
                     properties.nsid,
                     nid_type,
                     properties.nid,
                     tname,
                     iecid]
            self.model.AddRow(value)



il = None
fldridx = None    
fldropenidx = None
fileidx = None
smileidx = None
isz = (16,16)

treecolnames  = [  "Name", "Class", "NSIdx", "Id"]
treecolwidths = [     250,     100,      50,  200]


def prepare_image_list():
    global il, fldridx, fldropenidx, fileidx, smileidx    

    if il is not None: 
        return

    il = wx.ImageList(isz[0], isz[1])
    fldridx     = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FOLDER,      wx.ART_OTHER, isz))
    fldropenidx = il.Add(wx.ArtProvider.GetBitmap(wx.ART_FILE_OPEN,   wx.ART_OTHER, isz))
    fileidx     = il.Add(wx.ArtProvider.GetBitmap(wx.ART_NORMAL_FILE, wx.ART_OTHER, isz))
    smileidx    = il.Add(wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK, wx.ART_OTHER, isz))


AsyncUAClientLoop = None
def AsyncUAClientLoopProc():
    asyncio.set_event_loop(AsyncUAClientLoop)
    AsyncUAClientLoop.run_forever()

def ExecuteSychronously(func, timeout=1):
    def AsyncSychronizer(*args, **kwargs):
        global AsyncUAClientLoop
        # create asyncio loop
        if AsyncUAClientLoop is None:
            AsyncUAClientLoop = asyncio.new_event_loop()
            Thread(target=AsyncUAClientLoopProc, daemon=True).start()
        # schedule work in this loop
        future = asyncio.run_coroutine_threadsafe(func(*args, **kwargs), AsyncUAClientLoop)
        # wait max 5sec until connection completed
        return future.result(timeout)
    return AsyncSychronizer

def ExecuteSychronouslyWithTimeout(timeout):
    return functools.partial(ExecuteSychronously,timeout=timeout)


class OPCUAClientPanel(wx.SplitterWindow):
    def __init__(self, parent, modeldata, log, config_getter):
        self.log = log
        wx.SplitterWindow.__init__(self, parent, -1)

        self.ordered_nps = []

        self.inout_panel = wx.Panel(self)
        self.inout_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
        self.inout_sizer.AddGrowableCol(0)
        self.inout_sizer.AddGrowableRow(1)

        self.clientloop = None
        self.client = None
        self.config_getter = config_getter

        self.connect_button = wx.ToggleButton(self.inout_panel, -1, "Browse Server")

        self.selected_splitter = wx.SplitterWindow(self.inout_panel, style=wx.SUNKEN_BORDER | wx.SP_3D)

        self.selected_datas = modeldata
        self.selected_models = { direction:OPCUASubListModel(self.selected_datas[direction], log) for direction in directions }
        self.selected_lists = { direction:OPCUASubListPanel(
                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.connect_button, flag=wx.GROW)
        self.inout_sizer.Add(self.selected_splitter, flag=wx.GROW)
        self.inout_sizer.Layout()
        self.inout_panel.SetAutoLayout(True)
        self.inout_panel.SetSizer(self.inout_sizer)

        self.Initialize(self.inout_panel)

        self.Bind(wx.EVT_TOGGLEBUTTON, self.OnConnectButton, self.connect_button)

    def OnClose(self):
        if self.client is not None:
            asyncio.run(self.client.disconnect())
            self.client = None

    def __del__(self):
        self.OnClose()

    async def GetAsyncUANodeProperties(self, node):
        properties = type("UANodeProperties",(),dict(
                nsid = node.nodeid.NamespaceIndex,
                nid =  node.nodeid.Identifier,
                dname = (await node.read_display_name()).Text,
                cname = (await node.read_node_class()).name,
            ))
        if properties.cname == "Variable":
            properties.access = await node.get_access_level()
            properties.variant_type = (await node.read_data_type_as_variant_type()).name
        return properties

    @ExecuteSychronouslyWithTimeout(5)
    async def ConnectAsyncUAClient(self, config):
        client = Client(config["URI"])
        
        AuthType = config["AuthType"]
        if AuthType=="UserPasword":
            await client.set_user(config["User"])
            await client.set_password(config["Password"])
        elif AuthType=="x509":
            await client.set_security_string(
                "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config))

        await client.connect()
        self.client = client

        # load definition of server specific structures/extension objects
        await self.client.load_type_definitions()

        # returns root node object and its properties
        rootnode = self.client.get_root_node()
        return rootnode, await self.GetAsyncUANodeProperties(rootnode)

    @ExecuteSychronously
    async def DisconnectAsyncUAClient(self):
        if self.client is not None:
            await self.client.disconnect()
            self.client = None

    @ExecuteSychronously
    async def GetAsyncUANodeChildren(self, node):
        children = await node.get_children()
        return [ (child, await self.GetAsyncUANodeProperties(child)) for child in children]

    def OnConnectButton(self, event):
        if self.connect_button.GetValue():
            
            config = self.config_getter()
            self.log("OPCUA browser: connecting to {}\n".format(config["URI"]))
            
            try :
                rootnode, rootnodeproperties = self.ConnectAsyncUAClient(config)
            except Exception as e:
                self.log("Exception in OPCUA browser: "+repr(e)+"\n")
                self.client = None
                self.connect_button.SetValue(False)
                return

            self.tree_panel = wx.Panel(self)
            self.tree_sizer = wx.FlexGridSizer(cols=1, hgap=0, rows=2, vgap=0)
            self.tree_sizer.AddGrowableCol(0)
            self.tree_sizer.AddGrowableRow(0)

            self.tree = gizmos.TreeListCtrl(self.tree_panel, -1, style=0, agwStyle=
                                            gizmos.TR_DEFAULT_STYLE
                                            | gizmos.TR_MULTIPLE
                                            | gizmos.TR_FULL_ROW_HIGHLIGHT
                                       )

            prepare_image_list()
            self.tree.SetImageList(il)

            for idx,(colname, width) in enumerate(zip(treecolnames, treecolwidths)):
                self.tree.AddColumn(colname)
                self.tree.SetColumnWidth(idx, width)

            self.tree.SetMainColumn(0)

            rootitem = self.AddNodeItem(self.tree.AddRoot, rootnode, rootnodeproperties)

            # Populate first level so that root can be expanded
            self.CreateSubItems(rootitem)

            self.tree.Bind(wx.EVT_TREE_ITEM_EXPANDED, self.OnExpand)

            self.tree.Bind(wx.EVT_TREE_SEL_CHANGED, self.OnTreeNodeSelection)
            self.tree.Bind(wx.EVT_TREE_BEGIN_DRAG, self.OnTreeBeginDrag)

            self.tree.Expand(rootitem)

            hint = wx.StaticText(self.tree_panel, label = "Drag'n'drop desired variables from tree to Input or Output list")

            self.tree_sizer.Add(self.tree, flag=wx.GROW)
            self.tree_sizer.Add(hint, flag=wx.GROW)
            self.tree_sizer.Layout()
            self.tree_panel.SetAutoLayout(True)
            self.tree_panel.SetSizer(self.tree_sizer)

            self.SplitVertically(self.tree_panel, self.inout_panel, 500)
        else:
            self.DisconnectAsyncUAClient()
            self.Unsplit(self.tree_panel)
            self.tree_panel.Destroy()

    def CreateSubItems(self, item):
        node, properties, browsed = self.tree.GetPyData(item)
        if not browsed:
            children = self.GetAsyncUANodeChildren(node)
            for subnode, subproperties in children:
                self.AddNodeItem(lambda n: self.tree.AppendItem(item, n), subnode, subproperties)
            self.tree.SetPyData(item,(node, properties, True))

    def AddNodeItem(self, item_creation_func, node, properties):
        item = item_creation_func(properties.dname)

        if properties.cname == "Variable":
            access = properties.access
            normalidx = fileidx
            r = ua.AccessLevel.CurrentRead in access
            w = ua.AccessLevel.CurrentWrite in access
            if r and w:
                ext = "RW"
            elif r:
                ext = "RO"
            elif w:
                ext = "WO"  # not sure this one exist
            else:
                ext = "no access"  # not sure this one exist
            cname = "Var "+properties.variant_type+" (" + ext + ")"
        else:
            normalidx = fldridx

        self.tree.SetPyData(item,(node, properties, False))
        self.tree.SetItemText(item, properties.cname, 1)
        self.tree.SetItemText(item, str(properties.nsid), 2)
        self.tree.SetItemText(item, type(properties.nid).__name__+": "+str(properties.nid), 3)
        self.tree.SetItemImage(item, normalidx, which = wx.TreeItemIcon_Normal)
        self.tree.SetItemImage(item, fldropenidx, which = wx.TreeItemIcon_Expanded)

        return item

    def OnExpand(self, evt):
        for item in evt.GetItem().GetChildren():
            self.CreateSubItems(item)

    # def OnActivate(self, evt):
    #     item = evt.GetItem()
    #     node, browsed = self.tree.GetPyData(item)

    def OnTreeNodeSelection(self, event):
        items = self.tree.GetSelections()
        items_pydata = [self.tree.GetPyData(item) for item in items]

        nps = [(node,properties) for node, properties, unused in items_pydata]

        # append new nodes to ordered list
        for np in nps:
            if np not in self.ordered_nps:
                self.ordered_nps.append(np)

        # filter out vanished items
        self.ordered_nps = [
            np 
            for np in self.ordered_nps 
            if np in nps]

    def GetSelectedNodes(self):
        return self.ordered_nps 

    def OnTreeBeginDrag(self, event):
        """
        Called when a drag is started in tree
        @param event: wx.TreeEvent
        """
        if self.ordered_nps:
            # Just send a recognizable mime-type, drop destination
            # will get python data from parent
            data = wx.CustomDataObject(OPCUAClientDndMagicWord)
            dragSource = wx.DropSource(self)
            dragSource.SetData(data)
            dragSource.DoDragDrop()

    def Reset(self):
        for direction in directions:
            self.selected_models[direction].ResetData() 
        

class OPCUAClientList(list):
    def __init__(self, log, change_callback):
        super(OPCUAClientList, self).__init__(self)
        self.log = log
        self.change_callback = change_callback

    def append(self, value):
        v = dict(list(zip(lstcolnames, value)))

        if type(v["IEC"]) != int:
            if len(self) == 0:
                v["IEC"] = 0
            else:
                iecnums = set(zip(*self)[lstcolnames.index("IEC")])
                greatest = max(iecnums)
                holes = set(range(greatest)) - iecnums
                v["IEC"] = min(holes) if holes else greatest+1

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

        try:
            for t,n in zip(lstcoltypess, lstcolnames):
                v[n] = t(v[n]) 
        except ValueError: 
            self.log("Variable {} (Id={}) has invalid type\n".format(v["Name"],v["Id"]))
            return False

        if len(self)>0 and v["Id"] in list(zip(*self))[lstcolnames.index("Id")]:
            self.log("Variable {} (Id={}) already in list\n".format(v["Name"],v["Id"]))
            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 OPCUAClientModel(dict):
    def __init__(self, log, change_callback = lambda : None):
        super(OPCUAClientModel, self).__init__()
        for direction in directions:
            self[direction] = OPCUAClientList(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.items()}
            for direction, model in self.items():
                self[direction][:] = []
            for row in reader:
                direction = row[0]
                # avoids calling change callback whe 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 OPC-UA 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 UA_INLINE UA_ByteString
loadFile(const char *const path) {{
    UA_ByteString fileContents = UA_STRING_NULL;

    FILE *fp = fopen(path, "rb");
    if(!fp) {{
        errno = 0;
        LogError("OPC-UA could not open %s", path);
        return fileContents;
    }}

    fseek(fp, 0, SEEK_END);
    fileContents.length = (size_t)ftell(fp);
    fileContents.data = (UA_Byte *)UA_malloc(fileContents.length * sizeof(UA_Byte));
    if(fileContents.data) {{
        fseek(fp, 0, SEEK_SET);
        size_t read = fread(fileContents.data, sizeof(UA_Byte), fileContents.length, fp);
        if(read != fileContents.length){{
            UA_ByteString_clear(&fileContents);
            LogError("OPC-UA could not read %s", path);
        }}
    }} else {{
        fileContents.length = 0;
        LogError("OPC-UA Not enough memoty to load %s", path);
    }}
    fclose(fp);

    return fileContents;
}}

static UA_Client *client;
static UA_ClientConfig *cc;

#define DECL_VAR(ua_type, C_type, c_loc_name)                                                       \\
static UA_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)
{{
    UA_Client_disconnect(client);
    UA_Client_delete(client);
}}

#define INIT_NoAuth()                                                                              \\
    LogInfo("OPC-UA Init no auth");                                                                \\
    UA_ClientConfig_setDefault(cc);                                                                \\
    retval = UA_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-UA Init x509 %s,%s,%s,%s", #Policy, #UpperCaseMode, PrivateKey, Certificate);     \\
                                                                                                   \\
    UA_ByteString certificate = loadFile(Certificate);                                             \\
    UA_ByteString privateKey  = loadFile(PrivateKey);                                              \\
                                                                                                   \\
    cc->securityMode = UA_MESSAGESECURITYMODE_##UpperCaseMode;                                     \\
                                                                                                   \\
    /* replacement for default behaviour */                                                        \\
    /* UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey, NULL, 0, NULL, 0); */     \\
    do{{                                                                                           \\
        retval = UA_ClientConfig_setDefault(cc);                                                   \\
        if(retval != UA_STATUSCODE_GOOD)                                                           \\
            break;                                                                                 \\
                                                                                                   \\
        UA_SecurityPolicy *sp = (UA_SecurityPolicy*)                                               \\
            UA_realloc(cc->securityPolicies, sizeof(UA_SecurityPolicy) * 2);                       \\
        if(!sp){{                                                                                  \\
            retval = UA_STATUSCODE_BADOUTOFMEMORY;                                                 \\
            break;                                                                                 \\
        }}                                                                                         \\
        cc->securityPolicies = sp;                                                                 \\
                                                                                                   \\
        retval = UA_SecurityPolicy_##Policy(&cc->securityPolicies[cc->securityPoliciesSize],       \\
                                                 certificate, privateKey, &cc->logger);            \\
        if(retval != UA_STATUSCODE_GOOD) {{                                                        \\
            UA_LOG_WARNING(&cc->logger, UA_LOGCATEGORY_USERLAND,                                   \\
                           "Could not add SecurityPolicy Policy with error code %s",               \\
                           UA_StatusCode_name(retval));                                            \\
            UA_free(cc->securityPolicies);                                                         \\
            cc->securityPolicies = NULL;                                                           \\
            break;                                                                                 \\
        }}                                                                                         \\
                                                                                                   \\
        ++cc->securityPoliciesSize;                                                                \\
    }} while(0);                                                                                   \\
                                                                                                   \\
    retval = UA_Client_connect(client, uri);                                                       \\
                                                                                                   \\
    UA_ByteString_clear(&certificate);                                                             \\
    UA_ByteString_clear(&privateKey);

#define INIT_UserPassword(User, Password)                                                          \\
    LogInfo("OPC-UA Init UserPassword %s,%s", User, Password);                                     \\
    UA_ClientConfig_setDefault(cc);                                                                \\
    retval = UA_Client_connectUsername(client, uri, User, Password);

#define INIT_READ_VARIANT(ua_type, c_loc_name)                                                     \\
    UA_Variant_init(&c_loc_name##_variant);

#define INIT_WRITE_VARIANT(ua_type, ua_type_enum, c_loc_name)                                      \\
    UA_Variant_setScalar(&c_loc_name##_variant, (ua_type*)c_loc_name, &UA_TYPES[ua_type_enum]);

int __init_{locstr}(int argc,char **argv)
{{
    UA_StatusCode retval;
    client = UA_Client_new();
    cc = UA_Client_getConfig(client);
    char *uri = "{uri}";
{init}

    if(retval != UA_STATUSCODE_GOOD) {{
        LogError("OPC-UA Init Failed %d", retval);
        UA_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 = UA_Client_readValueAttribute(                                                         \\
        client, ua_nodeid_type(ua_nsidx, ua_node_id), &c_loc_name##_variant);                      \\
    if(retval == UA_STATUSCODE_GOOD && UA_Variant_isScalar(&c_loc_name##_variant) &&               \\
       c_loc_name##_variant.type == &UA_TYPES[ua_type_enum]) {{                                    \\
            c_loc_name##_buf = *(ua_type*)c_loc_name##_variant.data;                               \\
            UA_Variant_clear(&c_loc_name##_variant);  /* Unalloc requiered on each read ! */       \\
    }}

void __retrieve_{locstr}(void)
{{
    UA_StatusCode retval;
{retrieve}
}}

#define WRITE_VALUE(ua_type, c_loc_name, ua_nodeid_type, ua_nsidx, ua_node_id)                     \\
    UA_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":
            config["UpperCaseMode"] = config["Mode"].upper()
            formatdict["init"] += """
    INIT_x509({Policy}, {UpperCaseMode}, "{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 = UA_IEC_types[ua_type]
                c_loc_name = iec_direction_prefix + iec_size_prefix + locstr + "_" + str(iec_number)
                ua_nodeid_type, id_formating = UA_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, "OPCUA Client Test App", size=(800,600))

    argc = len(sys.argv)

    config={}
    config["URI"] = sys.argv[1] if argc>1 else "opc.tcp://localhost:4840"

    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 = OPCUAClientModel(print)

    opcuatestpanel = OPCUAClientPanel(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)
            opcuatestpanel.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(opcuatestpanel, flag=wx.GROW)

    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):
        opcuatestpanel.OnClose()
        evt.Skip()

    frame.Bind(wx.EVT_CLOSE, OnClose)

    frame.Show()

    app.MainLoop()