# HG changeset patch # User Edouard Tisserant # Date 1662491196 -7200 # Node ID a0b645a934c9d400735d31fa11dfc6b1eb37ec2f # Parent 412090a6b3a71cb73e926ce6fdddffbed9f92ebd OPC-UA, IDE: add CTN parameters to support OPC-UA encryption and authentication. xmlclass had to be enhanced to support elements with no content in xsd:choice diff -r 412090a6b3a7 -r a0b645a934c9 editors/ConfTreeNodeEditor.py --- a/editors/ConfTreeNodeEditor.py Tue Aug 23 08:39:08 2022 +0200 +++ b/editors/ConfTreeNodeEditor.py Tue Sep 06 21:06:36 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 412090a6b3a7 -r a0b645a934c9 opc_ua/client.py --- a/opc_ua/client.py Tue Aug 23 08:39:08 2022 +0200 +++ b/opc_ua/client.py Tue Sep 06 21:06:36 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,18 @@ 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: + res[name] = cfg("AuthType."+name) + + print(res) + return res def GetFileName(self): return os.path.join(self.CTNPath(), 'selected.csv') @@ -76,8 +125,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) diff -r 412090a6b3a7 -r a0b645a934c9 opc_ua/opcua_client_maker.py --- a/opc_ua/opcua_client_maker.py Tue Aug 23 08:39:08 2022 +0200 +++ b/opc_ua/opcua_client_maker.py Tue Sep 06 21:06:36 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,12 @@ self.tree.SetMainColumn(0) - self.client = Client(self.uri_getter()) + config = self.config_getter() + self.client = Client(config["URI"]) + + # TODO: crypto stuff + # client.set_security_string("Basic256Sha256,SignAndEncrypt,certificate-example.der,private-key-example.pem") + self.client.connect() self.client.load_type_definitions() # load definition of server specific structures/extension objects rootnode = self.client.get_root_node() @@ -478,7 +493,7 @@ 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 @@ -492,65 +507,66 @@ 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_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 +{init} /* Connect to server */ - retval = UA_Client_connect(client, "%(uri)s"); - if(retval != UA_STATUSCODE_GOOD) { + retval = UA_Client_connect(client, "{uri}"); + 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"], + # TODO: pass authentication code. decl = "", cleanup = "", init = "", @@ -581,7 +597,7 @@ 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 +610,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 +632,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 +653,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 412090a6b3a7 -r a0b645a934c9 tests/projects/opcua_client/opcua_0@opcua/confnode.xml --- a/tests/projects/opcua_client/opcua_0@opcua/confnode.xml Tue Aug 23 08:39:08 2022 +0200 +++ b/tests/projects/opcua_client/opcua_0@opcua/confnode.xml Tue Sep 06 21:06:36 2022 +0200 @@ -1,2 +1,9 @@ - + + + + + + + + diff -r 412090a6b3a7 -r a0b645a934c9 xmlclass/xmlclass.py --- a/xmlclass/xmlclass.py Tue Aug 23 08:39:08 2022 +0200 +++ b/xmlclass/xmlclass.py Tue Sep 06 21:06:36 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):