# HG changeset patch # User Edouard Tisserant # Date 1663080714 -7200 # Node ID 02229133df43e858f713f511ae9525adc43d4c14 # Parent 50600b946ea7703c41db842ed1ea3604b640c72b# Parent cfbdccad71a58d908e1490d5524a04643c9a9398 merge diff -r cfbdccad71a5 -r 02229133df43 editors/CodeFileEditor.py --- a/editors/CodeFileEditor.py Tue Sep 13 16:32:39 2022 +0200 +++ b/editors/CodeFileEditor.py Tue Sep 13 16:51:54 2022 +0200 @@ -824,12 +824,12 @@ type_menu = wx.Menu(title='') base_menu = wx.Menu(title='') for base_type in self.Controler.GetBaseTypes(): - new_entry = base_menu.Append(help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL, text=base_type) + new_entry = base_menu.Append(wx.ID_ANY, base_type) self.Bind(wx.EVT_MENU, self.GetVariableTypeFunction(base_type), new_entry) type_menu.AppendMenu(wx.ID_ANY, "Base Types", base_menu) datatype_menu = wx.Menu(title='') for datatype in self.Controler.GetDataTypes(): - new_entry = datatype_menu.Append(help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL, text=datatype) + new_entry = datatype_menu.Append(wx.ID_ANY, item=datatype) self.Bind(wx.EVT_MENU, self.GetVariableTypeFunction(datatype), new_entry) type_menu.AppendMenu(wx.ID_ANY, "User Data Types", datatype_menu) rect = self.VariablesGrid.BlockToDeviceRect((row, col), (row, col)) diff -r cfbdccad71a5 -r 02229133df43 editors/ConfTreeNodeEditor.py --- a/editors/ConfTreeNodeEditor.py Tue Sep 13 16:32:39 2022 +0200 +++ b/editors/ConfTreeNodeEditor.py Tue Sep 13 16:51:54 2022 +0200 @@ -435,13 +435,16 @@ name = element_infos["name"] value = element_infos["value"] - staticbox = wx.StaticBox(self.ParamsEditor, - label="%s - %s" % (_(name), _(value)), - size=wx.Size(10, 0)) - staticboxsizer = wx.StaticBoxSizer(staticbox, wx.VERTICAL) - sizer.Add(staticboxsizer, border=5, flag=wx.GROW | wx.BOTTOM | wx.LEFT | wx.RIGHT) - self.GenerateSizerElements(staticboxsizer, element_infos["children"], element_path) - callback = self.GetChoiceContentCallBackFunction(combobox, staticboxsizer, element_path) + staticboxsizer = None + if element_infos["children"]: + staticbox = wx.StaticBox(self.ParamsEditor, + label="%s - %s" % (_(name), _(value)), + size=wx.Size(10, 0)) + staticboxsizer = wx.StaticBoxSizer(staticbox, wx.VERTICAL) + sizer.Add(staticboxsizer, border=5, flag=wx.GROW | wx.BOTTOM | wx.LEFT | wx.RIGHT) + self.GenerateSizerElements(staticboxsizer, element_infos["children"], element_path) + + callback = self.GetChoiceContentCallBackFunction(combobox, element_path) else: for choice in element_infos["type"]: combobox.Append(choice) @@ -527,7 +530,7 @@ textctrl.Bind(wx.EVT_TEXT, callback) textctrl.Bind(wx.EVT_KILL_FOCUS, callback) - if not isinstance(element_infos["type"], list) and element_infos["use"] == "optional": + if not isinstance(element_infos["type"], list) and element_infos.get("use", None) == "optional": bt = wx.BitmapButton(self.ParamsEditor, bitmap=wx.ArtProvider.GetBitmap(wx.ART_UNDO, wx.ART_TOOLBAR, (16,16)), style=wx.BORDER_NONE) @@ -579,7 +582,7 @@ event.Skip() return OnChoiceChanged - def GetChoiceContentCallBackFunction(self, choicectrl, staticboxsizer, path): + def GetChoiceContentCallBackFunction(self, choicectrl, path): def OnChoiceContentChanged(event): self.SetConfNodeParamsAttribute(path, choicectrl.GetStringSelection()) wx.CallAfter(self.RefreshConfNodeParamsSizer) diff -r cfbdccad71a5 -r 02229133df43 opc_ua/client.py --- a/opc_ua/client.py Tue Sep 13 16:32:39 2022 +0200 +++ b/opc_ua/client.py Tue Sep 13 16:51:54 2022 +0200 @@ -6,7 +6,7 @@ from editors.ConfTreeNodeEditor import ConfTreeNodeEditor from PLCControler import LOCATION_CONFNODE, LOCATION_VAR_INPUT, LOCATION_VAR_OUTPUT -from .opcua_client_maker import OPCUAClientPanel, OPCUAClientModel, UA_IEC_types +from .opcua_client_maker import OPCUAClientPanel, OPCUAClientModel, UA_IEC_types, authParams import util.paths as paths @@ -29,17 +29,56 @@ def Log(self, msg): self.Controler.GetCTRoot().logger.write(msg) - def UriGetter(self): - return self.Controler.GetServerURI() - def CreateOPCUAClient_UI(self, parent): - return OPCUAClientPanel(parent, self.Controler.GetModelData(), self.Log, self.UriGetter) + return OPCUAClientPanel(parent, self.Controler.GetModelData(), self.Log, self.Controler.GetConfig) class OPCUAClient(object): XSD = """ + + + + + + + + + + Default to Basic256Sha256 if not specified + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -61,8 +100,23 @@ def GetModelData(self): return self.modeldata - def GetServerURI(self): - return self.GetParamsAttributes("OPCUAClient.Server_URI")["value"] + def GetConfig(self): + cfg = lambda path: self.GetParamsAttributes("OPCUAClient."+path)["value"] + AuthType = cfg("AuthType") + res = dict(URI=cfg("Server_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') @@ -76,8 +130,7 @@ locstr = "_".join(map(str, current_location)) c_path = os.path.join(buildpath, "opcua_client__%s.c" % locstr) - c_code = self.modeldata.GenerateC(c_path, locstr, - self.GetParamsAttributes("OPCUAClient.Server_URI")["value"]) + c_code = self.modeldata.GenerateC(c_path, locstr, self.GetConfig()) with open(c_path, 'wb') as c_file: c_file.write(c_code) @@ -86,6 +139,8 @@ CFLAGS = ' '.join(['-I"' + path + '"' for path in Open62541IncludePaths]) + # Note: all user provided files are systematicaly copied, including cryptomaterial + return [(c_path, CFLAGS)], LDFLAGS, True def GetVariableLocationTree(self): diff -r cfbdccad71a5 -r 02229133df43 opc_ua/opcua_client_maker.py --- a/opc_ua/opcua_client_maker.py Tue Sep 13 16:32:39 2022 +0200 +++ b/opc_ua/opcua_client_maker.py Tue Sep 13 16:51:54 2022 +0200 @@ -38,6 +38,16 @@ 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)) @@ -230,7 +240,7 @@ class OPCUAClientPanel(wx.SplitterWindow): - def __init__(self, parent, modeldata, log, uri_getter): + def __init__(self, parent, modeldata, log, config_getter): self.log = log wx.SplitterWindow.__init__(self, parent, -1) @@ -242,7 +252,7 @@ self.inout_sizer.AddGrowableRow(1) self.client = None - self.uri_getter = uri_getter + self.config_getter = config_getter self.connect_button = wx.ToggleButton(self.inout_panel, -1, "Browse Server") @@ -298,7 +308,17 @@ self.tree.SetMainColumn(0) - self.client = Client(self.uri_getter()) + config = self.config_getter() + self.client = Client(config["URI"]) + + AuthType = config["AuthType"] + if AuthType=="UserPasword": + self.client.set_user(config["User"]) + self.client.set_password(config["Password"]) + elif AuthType=="x509": + self.client.set_security_string( + "{Policy},{Mode},{Certificate},{PrivateKey}".format(**config)) + self.client.connect() self.client.load_type_definitions() # load definition of server specific structures/extension objects rootnode = self.client.get_root_node() @@ -478,85 +498,151 @@ for row in data: writer.writerow([direction] + row) - def GenerateC(self, path, locstr, server_uri): + def GenerateC(self, path, locstr, config): template = """/* code generated by beremiz OPC-UA extension */ #include #include #include +#include + +#include +#include + +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; + 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); + }} else {{ + fileContents.length = 0; + }} + 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)s - -void __cleanup_%(locstr)s(void) -{ +{decl} + +void __cleanup_{locstr}(void) +{{ UA_Client_disconnect(client); UA_Client_delete(client); -} - +}} + + +#define INIT_NoAuth() \\ + UA_ClientConfig_setDefault(cc); \\ + retval = UA_Client_connect(client, uri); + +/* Note : Policy is ignored here since open62541 client supports all policies by default */ +#define INIT_x509(Policy, UpperCaseMode, PrivateKey, Certificate) \\ + /* TODO try paths given in runtime CLI */ \\ + UA_ByteString certificate = loadFile(Certificate); \\ + UA_ByteString privateKey = loadFile(PrivateKey); \\ + \\ + printf("INIT_x509 %s,%s,%s,%s\\n", #Policy, #UpperCaseMode, PrivateKey, Certificate); \\ + cc->securityMode = UA_MESSAGESECURITYMODE_##UpperCaseMode; \\ + UA_ClientConfig_setDefaultEncryption(cc, certificate, privateKey, NULL, 0, NULL, 0); \\ + \\ + retval = UA_Client_connect(client, uri); \\ + \\ + UA_ByteString_clear(&certificate); \\ + UA_ByteString_clear(&privateKey); + +#define INIT_UserPassword(User, Password) \\ + printf("TODO INIT_UserPassword %s,%s\\n", User, Password); \\ + 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) \\ +#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)s(int argc,char **argv) -{ +int __init_{locstr}(int argc,char **argv) +{{ UA_StatusCode retval; client = UA_Client_new(); - UA_ClientConfig_setDefault(UA_Client_getConfig(client)); -%(init)s - - /* Connect to server */ - retval = UA_Client_connect(client, "%(uri)s"); - if(retval != UA_STATUSCODE_GOOD) { + cc = UA_Client_getConfig(client); + char *uri = "{uri}"; +{init} + + if(retval != UA_STATUSCODE_GOOD) {{ UA_Client_delete(client); return EXIT_FAILURE; - } -} + }} +}} #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##_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)s(void) -{ + }} + +void __retrieve_{locstr}(void) +{{ UA_StatusCode retval; -%(retrieve)s -} - -#define WRITE_VALUE(ua_type, c_loc_name, ua_nodeid_type, ua_nsidx, ua_node_id) \\ +{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)s(void) -{ -%(publish)s -} +void __publish_{locstr}(void) +{{ +{publish} +}} """ formatdict = dict( locstr = locstr, - uri = server_uri, + 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.iteritems(): iec_direction_prefix = {"input": "__I", "output": "__Q"}[direction] for row in data: @@ -570,18 +656,18 @@ DECL_VAR({ua_type}, {C_type}, {c_loc_name})""".format(**locals()) if direction == "input": - formatdict["init"] +=""" + 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"] +=""" + 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%formatdict + Ccode = template.format(**formatdict) return Ccode @@ -594,7 +680,20 @@ frame = wx.Frame(None, -1, "OPCUA Client Test App", size=(800,600)) - uri = sys.argv[1] if len(sys.argv)>1 else "opc.tcp://localhost:4840" + 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 izip_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) @@ -603,7 +702,7 @@ modeldata = OPCUAClientModel(print) - opcuatestpanel = OPCUAClientPanel(test_panel, modeldata, print, lambda:uri) + opcuatestpanel = OPCUAClientPanel(test_panel, modeldata, print, lambda:config) def OnGenerate(evt): dlg = wx.FileDialog( @@ -624,7 +723,7 @@ -I ../../open62541/arch/ ../../open62541/build/bin/libopen62541.a */ -"""%(path, path[:-2]) + modeldata.GenerateC(path, "test", uri) + """ +"""%(path, path[:-2]) + modeldata.GenerateC(path, "test", config) + """ int main(int argc, char *argv[]) { diff -r cfbdccad71a5 -r 02229133df43 tests/projects/opcua_client/opcua_0@opcua/confnode.xml --- a/tests/projects/opcua_client/opcua_0@opcua/confnode.xml Tue Sep 13 16:32:39 2022 +0200 +++ b/tests/projects/opcua_client/opcua_0@opcua/confnode.xml Tue Sep 13 16:51:54 2022 +0200 @@ -1,2 +1,9 @@ - + + + + + + + + diff -r cfbdccad71a5 -r 02229133df43 xmlclass/xmlclass.py --- a/xmlclass/xmlclass.py Tue Sep 13 16:32:39 2022 +0200 +++ b/xmlclass/xmlclass.py Tue Sep 13 16:51:54 2022 +0200 @@ -598,11 +598,15 @@ else: return "" + def Initial(): + p = etree.Element(infos["name"]) + return p + return { "type": TAG, "extract": ExtractTag, "generate": GenerateTag, - "initial": lambda: None, + "initial": Initial, "check": lambda x: x is None or infos["minOccurs"] == 0 and x } @@ -1450,14 +1454,14 @@ parts = path.split(".", 1) if parts[0] in attributes: if len(parts) != 1: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) attr_type = gettypeinfos(attributes[parts[0]]["attr_type"]["basename"], attributes[parts[0]]["attr_type"]["facets"]) value = getattr(self, parts[0], "") elif parts[0] in elements: if elements[parts[0]]["elmt_type"]["type"] == SIMPLETYPE: if len(parts) != 1: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) attr_type = gettypeinfos(elements[parts[0]]["elmt_type"]["basename"], elements[parts[0]]["elmt_type"]["facets"]) value = getattr(self, parts[0], "") @@ -1466,7 +1470,7 @@ else: attr = getattr(self, parts[0], None) if attr is None: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) if len(parts) == 1: return attr.getElementInfos(parts[0]) else: @@ -1477,7 +1481,7 @@ elif "base" in classinfos: classinfos["base"].getElementInfos(name, path) else: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) else: if not derived: children.extend(self.getElementAttributes()) @@ -1519,7 +1523,7 @@ parts = path.split(".", 1) if parts[0] in attributes: if len(parts) != 1: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) if attributes[parts[0]]["attr_type"]["basename"] == "boolean": setattr(self, parts[0], value) elif attributes[parts[0]]["use"] == "optional" and value == None: @@ -1534,7 +1538,7 @@ elif parts[0] in elements: if elements[parts[0]]["elmt_type"]["type"] == SIMPLETYPE: if len(parts) != 1: - raise ValueError("Wrong path!") + raise ValueError("Wrong path: "+path) if elements[parts[0]]["elmt_type"]["basename"] == "boolean": setattr(self, parts[0], value) elif attributes[parts[0]]["minOccurs"] == 0 and value == "": @@ -1738,6 +1742,8 @@ def tostring(self): return NAMESPACE_PATTERN.sub("", etree.tostring(self, pretty_print=True, encoding='utf-8')).decode('utf-8') + def getElementInfos(self, name, path=None, derived=False): + return {"name": name, "type": TAG, "value": None, "use": None, "children": []} class XMLElementClassLookUp(etree.PythonElementClassLookup):