Commit 2566969b authored by ORD's avatar ORD

Merge pull request #114 from iirob/master

Server side deleting of nodes.
parents 6ee90273 96958774
import sys
sys.path.insert(0, "..")
import logging
import time
try:
from IPython import embed
except ImportError:
import code
def embed():
vars = globals()
vars.update(locals())
shell = code.InteractiveConsole(vars)
shell.interact()
from opcua import Client
from opcua import ua
class SubHandler(object):
"""
Subscription Handler. To receive events from server for a subscription
data_change and event methods are called directly from receiving thread.
Do not do expensive, slow or network operation there. Create another
thread if you need to do such a thing
"""
def datachange_notification(self, node, val, data):
print("Python: New data change event", node, val)
def event_notification(self, event):
print("Python: New event", event)
def status_change_notification(self, status):
print ("Python: New status change", status)
if __name__ == "__main__":
logging.basicConfig(level=logging.WARN)
#logger = logging.getLogger("KeepAlive")
#logger.setLevel(logging.DEBUG)
#client = Client("opc.tcp://localhost:4840/freeopcua/server/")
client = Client("opc.tcp://admin@localhost:4840/freeopcua/server/") #connect using a user
try:
client.connect()
# Client has a few methods to get proxy to UA nodes that should always be in address space such as Root or Objects
root = client.get_root_node()
print("Root node is: ", root)
objects = client.get_objects_node()
print("Objects node is: ", objects)
# Node objects have methods to read and write node attributes as well as browse or populate address space
print("Children of root are: ", root.get_children())
# get a specific node knowing its node id
#var = client.get_node(ua.NodeId(1002, 2))
#var = client.get_node("ns=3;i=2002")
#print(var)
#var.get_data_value() # get value of node as a DataValue object
#var.get_value() # get value of node as a python builtin
#var.set_value(ua.Variant([23], ua.VariantType.Int64)) #set node value using explicit data type
#var.set_value(3.9) # set node value using implicit data type
# Now getting a variable node using its browse path
myvar = root.get_child(["0:Objects", "2:MyObject", "2:MyVariable"])
obj = root.get_child(["0:Objects", "2:MyObject"])
print("myvar is: ", myvar)
# subscribing to a variable node
handler = SubHandler()
sub = client.create_subscription(500, handler)
handle = sub.subscribe_data_change(myvar)
time.sleep(0.1)
# we can also subscribe to events from server
sub.subscribe_events()
# sub.unsubscribe(handle)
# sub.delete()
# calling a method on server
res = obj.call_method("2:multiply", 3, "klk")
print("method result is: ", res)
print("Children of MyObject are: ", obj.get_children())
print("myvar should be still there")
deletenode = ua.DeleteNodesItem()
deletenode.NodeId = obj.get_child(["2:MyVariable"]).nodeid
deletenode.DeleteTargetReferences = True
results = client.bclient.delete_nodes([deletenode])
results[0].check()
print("Children of MyObject are: ", obj.get_children())
print("myvar should disapear")
embed()
finally:
client.disconnect()
......@@ -14,7 +14,7 @@ except ImportError:
shell.interact()
from opcua import ua, uamethod, Server, Event
from opcua import ua, uamethod, Server, Event
class SubHandler(object):
......
......@@ -423,13 +423,13 @@ class BinaryClient(object):
response = ua.AddNodesResponse.from_binary(data)
response.ResponseHeader.ServiceResult.check()
return response.Results
def delete_nodes(self, nodestodelete):
self.logger.info("delete_nodes")
request = ua.DeleteNodesRequest()
request.Parameters.NodesToDelete = nodestodelete
data = self._uasocket.send_request(request)
response = ua.AddNodesResponse.from_binary(data)
response = ua.DeleteNodesResponse.from_binary(data)
response.ResponseHeader.ServiceResult.check()
return response.Results
......
......@@ -285,7 +285,7 @@ class Client(object):
uris = []
params = ua.FindServersParameters()
params.EndpointUrl = self.server_url.geturl()
params.ServerUris = uris
params.ServerUris = uris
return self.bclient.find_servers(params)
def find_servers_on_network(self):
......@@ -357,7 +357,7 @@ class Client(object):
params.UserTokenSignature.Signature = sig
else:
params.UserIdentityToken = ua.UserNameIdentityToken()
params.UserIdentityToken.UserName = username
params.UserIdentityToken.UserName = username
if self.server_url.password:
pubkey = uacrypto.x509_from_der(self.security_policy.server_certificate).public_key()
# see specs part 4, 7.36.3: if the token is encrypted, password
......@@ -401,7 +401,7 @@ class Client(object):
handler argument is a class with data_change and/or event methods.
These methods will be called when notfication from server are received.
See example-client.py.
Do not do expensive/slow or network operation from these methods
Do not do expensive/slow or network operation from these methods
since they are called directly from receiving thread. This is a design choice,
start another thread if you need to do such a thing.
"""
......
......@@ -288,7 +288,7 @@ class Node(object):
# Hack for convenience methods
# local import is ugly but necessary for python2 support
# feel fri to propose something better but I want to split all those
# feel fri to propose something better but I want to split all those
# create methods fro Node
def add_folder(*args, **kwargs):
......@@ -314,4 +314,3 @@ class Node(object):
def call_method(*args, **kwargs):
from opcua.common import methods
return methods.call_method(*args, **kwargs)
......@@ -33,6 +33,12 @@ class SubHandler(object):
"""
pass
def status_change_notification(self, status):
"""
called for every status change notfication from server
"""
pass
def event(self, handle, event):
"""
Deprecated use event_notification
......@@ -82,7 +88,7 @@ class Subscription(object):
"""
Subscription object returned by Server or Client objects.
The object represent a subscription to an opc-ua server.
This is a high level class, especially subscribe_data_change
This is a high level class, especially subscribe_data_change
and subscribe_events methods. If more control is necessary look at
code and/or use create_monitored_items method.
"""
......@@ -176,7 +182,7 @@ class Subscription(object):
def _call_status(self, status):
try:
self._handler.status_change(status.Status)
self._handler.status_change_notification(status.Status)
except Exception:
self.logger.exception("Exception calling status change handler")
......@@ -211,7 +217,7 @@ class Subscription(object):
def subscribe_events(self, sourcenode=ua.ObjectIds.Server, evtype=ua.ObjectIds.BaseEventType):
"""
Subscribe to events from a node. Default node is Server node.
Subscribe to events from a node. Default node is Server node.
In most servers the server node is the only one you can subscribe to.
Return a handle which can be used to unsubscribe
"""
......@@ -265,14 +271,14 @@ class Subscription(object):
params.SubscriptionId = self.subscription_id
params.ItemsToCreate = monitored_items
params.TimestampsToReturn = ua.TimestampsToReturn.Neither
mids = []
results = self.server.create_monitored_items(params)
# FIXME: Race condition here
# We lock as early as possible. But in some situation, a notification may arrives before
# locking and we will not be able to prosess it. To avoid issues, users should subscribe
# locking and we will not be able to prosess it. To avoid issues, users should subscribe
# to all nodes at once
with self._lock:
with self._lock:
for idx, result in enumerate(results):
mi = params.ItemsToCreate[idx]
if not result.StatusCode.is_good():
......
......@@ -181,7 +181,7 @@ class NodeManagementService(object):
result = ua.AddNodesResult()
if item.RequestedNewNodeId in self._aspace:
self.logger.warning("AddNodeItem: node already exists")
self.logger.warning("AddNodesItem: node already exists")
result.StatusCode = ua.StatusCode(ua.StatusCodes.BadNodeIdExists)
return result
nodedata = NodeData(item.RequestedNewNodeId)
......@@ -233,6 +233,37 @@ class NodeManagementService(object):
return result
def delete_nodes(self, deletenodeitems, user=User.Admin):
results = []
for item in deletenodeitems:
results.append(self._delete_node(item, user))
return results
def _delete_node(self, item, user):
if not user == User.Admin:
return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
if item.NodeId not in self._aspace:
self.logger.warning("DeleteNodesItem: node does not exists")
return ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown)
if item.DeleteTargetReferences:
for elem in self._aspace.keys():
for rdesc in self._aspace[elem].references:
if rdesc.NodeId == item.NodeId:
self._aspace[elem].references.remove(rdesc)
for handle, callback in list(self._aspace[item.NodeId].attributes[ua.AttributeIds.Value].datachange_callbacks.items()):
try:
callback(handle, None, ua.StatusCode(ua.StatusCodes.BadNodeIdUnknown))
self._aspace.delete_datachange_callback(handle)
except Exception as ex:
self.logger.exception("Error calling datachange callback %s, %s, %s", k, v, ex)
del self._aspace[item.NodeId]
return ua.StatusCode()
def add_references(self, refs, user=User.Admin):
result = []
for ref in refs:
......@@ -260,6 +291,36 @@ class NodeManagementService(object):
self._aspace[addref.SourceNodeId].references.append(rdesc)
return ua.StatusCode()
def delete_references(self, refs, user=User.Admin):
result = []
for ref in refs:
result.append(self._delete_reference(ref, user))
return result
def _delete_reference(self, item, user):
if item.SourceNodeId not in self._aspace:
return ua.StatusCode(ua.StatusCodes.BadSourceNodeIdInvalid)
if item.TargetNodeId not in self._aspace:
return ua.StatusCode(ua.StatusCodes.BadTargetNodeIdInvalid)
if not user == User.Admin:
return ua.StatusCode(ua.StatusCodes.BadUserAccessDenied)
for rdesc in self._aspace[item.SourceNodeId].references:
if rdesc.NodeId is item.TargetNodeId:
if rdesc.RefrenceTypeId != item.RefrenceTypeId:
return ua.StatusCode(ua.StatusCode.BadReferenceTypeInvalid)
if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
self._aspace[item.SourceNodeId].references.remove(rdesc)
for rdesc in self._aspace[item.TargetNodeId].references:
if rdesc.NodeId is item.SourceNodeId:
if rdesc.RefrenceTypeId != item.RefrenceTypeId:
return ua.StatusCode(ua.StatusCode.BadReferenceTypeInvalid)
if rdesc.IsForward == item.IsForward or item.DeleteBidirectional:
self._aspace[item.SourceNodeId].references.remove(rdesc)
return ua.StatusCode()
def _add_node_attr(self, item, nodedata, name, vtype=None):
if item.SpecifiedAttributes & getattr(ua.NodeAttributesMask, name):
dv = ua.DataValue(ua.Variant(getattr(item, name), vtype))
......@@ -351,6 +412,14 @@ class AddressSpace(object):
with self._lock:
return self._nodes.__contains__(nodeid)
def __delitem__(self, nodeid):
with self._lock:
self._nodes.__delitem__(nodeid)
def keys(self):
with self._lock:
return self._nodes.keys()
def dump(self, path):
"""
dump address space as binary to file
......
......@@ -55,9 +55,9 @@ class InternalServer(object):
self.method_service = MethodService(self.aspace)
self.node_mgt_service = NodeManagementService(self.aspace)
# import address space from code generated from xml
standard_address_space.fill_address_space(self.node_mgt_service)
standard_address_space.fill_address_space(self.node_mgt_service)
# import address space from save db to disc
#standard_address_space.fill_address_space_from_disk(self.aspace)
#standard_address_space.fill_address_space_from_disk(self.aspace)
# import address space directly from xml, this has preformance impact so disabled
#importer = xmlimporter.XmlImporter(self.node_mgt_service)
......@@ -120,7 +120,7 @@ class InternalServer(object):
def find_servers(self, params):
if not params.ServerUris:
return [desc.Server for desc in self._known_servers.values()]
return [desc.Server for desc in self._known_servers.values()]
servers = []
for serv in self._known_servers.values():
serv_uri = serv.Server.ApplicationUri.split(":")
......@@ -215,8 +215,8 @@ class InternalSession(object):
return self.iserver.attribute_service.read(params)
def write(self, params):
if not self.external:
# If session is internal we need to store a copy og object, not a reference,
if not self.external:
# If session is internal we need to store a copy og object, not a reference,
#otherwise users may change it and we will not generate expected events
for ntw in params.NodesToWrite:
ntw.Value.Value.Value = copy(ntw.Value.Value.Value)
......@@ -231,9 +231,15 @@ class InternalSession(object):
def add_nodes(self, params):
return self.iserver.node_mgt_service.add_nodes(params, self.user)
def delete_nodes(self, params):
return self.iserver.node_mgt_service.delete_nodes(params, self.user)
def add_references(self, params):
return self.iserver.node_mgt_service.add_references(params, self.user)
def delete_references(self, params):
return self.iserver.node_mgt_service.delete_references(params, self.user)
def add_method_callback(self, methodid, callback):
return self.aspace.add_method_callback(methodid, callback)
......
......@@ -107,7 +107,7 @@ class MonitoredItemService(object):
if result.StatusCode.is_good():
# force data change event generation
self.trigger_datachange(handle, params.ItemToMonitor.NodeId, params.ItemToMonitor.AttributeId)
if not result.StatusCode.is_good():
del(self._monitored_items[result.MonitoredItemId])
self._monitored_item_counter -= 1
......@@ -137,15 +137,19 @@ class MonitoredItemService(object):
self._monitored_items.pop(mid)
return ua.StatusCode()
def datachange_callback(self, handle, value):
self.logger.info("subscription %s: datachange callback called with handle '%s' and value '%s'", self, handle, value.Value)
event = ua.MonitoredItemNotification()
with self._lock:
mid = self._monitored_datachange[handle]
mdata = self._monitored_items[mid]
event.ClientHandle = mdata.client_handle
event.Value = value
self.isub.enqueue_datachange_event(mid, event, mdata.parameters.RevisedQueueSize)
def datachange_callback(self, handle, value, error=None):
if error:
self.logger.info("subscription %s: datachange callback called with handle '%s' and erorr '%s'", self, handle, error)
self.trigger_statuschange(error)
else:
self.logger.info("subscription %s: datachange callback called with handle '%s' and value '%s'", self, handle, value.Value)
event = ua.MonitoredItemNotification()
with self._lock:
mid = self._monitored_datachange[handle]
mdata = self._monitored_items[mid]
event.ClientHandle = mdata.client_handle
event.Value = value
self.isub.enqueue_datachange_event(mid, event, mdata.parameters.RevisedQueueSize)
def trigger_event(self, event):
with self._lock:
......@@ -289,7 +293,7 @@ class InternalSubscription(object):
notif = ua.StatusChangeNotification()
notif.Status = self._triggered_statuschanges.pop(0)
result.NotificationMessage.NotificationData.append(notif)
self.logger.debug("sending event notification %s", len(notif.Status))
self.logger.debug("sending event notification %s", notif.Status)
def publish(self, nb):
with self._lock:
......
......@@ -256,6 +256,18 @@ class UAProcessor(object):
self.logger.info("sending add node response")
self.send_response(requesthdr.RequestHandle, algohdr, seqhdr, response)
elif typeid == ua.NodeId(ua.ObjectIds.DeleteNodesRequest_Encoding_DefaultBinary):
self.logger.info("delete nodes request")
params = ua.DeleteNodesParameters.from_binary(body)
results = self.session.delete_nodes(params.NodesToDelete)
response = ua.DeleteNodesResponse()
response.Results = results
self.logger.info("sending delete node response")
self.send_response(requesthdr.RequestHandle, algohdr, seqhdr, response)
elif typeid == ua.NodeId(ua.ObjectIds.CreateSubscriptionRequest_Encoding_DefaultBinary):
self.logger.info("create subscription request")
params = ua.CreateSubscriptionParameters.from_binary(body)
......
......@@ -760,19 +760,19 @@ class DiagnosticInfo(FrozenClass):
if self.InnerStatusCode: self.Encoding |= (1 << 5)
if self.InnerDiagnosticInfo: self.Encoding |= (1 << 6)
packet.append(uatype_UInt8.pack(self.Encoding))
if self.SymbolicId:
if self.SymbolicId:
packet.append(uatype_Int32.pack(self.SymbolicId))
if self.NamespaceURI:
if self.NamespaceURI:
packet.append(uatype_Int32.pack(self.NamespaceURI))
if self.Locale:
if self.Locale:
packet.append(uatype_Int32.pack(self.Locale))
if self.LocalizedText:
if self.LocalizedText:
packet.append(uatype_Int32.pack(self.LocalizedText))
if self.AdditionalInfo:
if self.AdditionalInfo:
packet.append(pack_bytes(self.AdditionalInfo))
if self.InnerStatusCode:
if self.InnerStatusCode:
packet.append(self.InnerStatusCode.to_binary())
if self.InnerDiagnosticInfo:
if self.InnerDiagnosticInfo:
packet.append(self.InnerDiagnosticInfo.to_binary())
return b''.join(packet)
......@@ -4930,8 +4930,14 @@ class DeleteNodesRequest(FrozenClass):
__repr__ = __str__
class DeleteNodesResult(FrozenClass):
class DeleteNodesResponse(FrozenClass):
'''
Delete one or more nodes from the server address space.
:ivar TypeId:
:vartype TypeId: NodeId
:ivar ResponseHeader:
:vartype ResponseHeader: ResponseHeader
:ivar Results:
:vartype Results: StatusCode
:ivar DiagnosticInfos:
......@@ -4942,12 +4948,16 @@ class DeleteNodesResult(FrozenClass):
self._binary_init(binary)
self._freeze = True
return
self.TypeId = FourByteNodeId(ObjectIds.DeleteNodesResponse_Encoding_DefaultBinary)
self.ResponseHeader = ResponseHeader()
self.Results = []
self.DiagnosticInfos = []
self._freeze = True
def to_binary(self):
packet = []
packet.append(self.TypeId.to_binary())
packet.append(self.ResponseHeader.to_binary())
packet.append(uatype_Int32.pack(len(self.Results)))
for fieldname in self.Results:
packet.append(fieldname.to_binary())
......@@ -4958,9 +4968,11 @@ class DeleteNodesResult(FrozenClass):
@staticmethod
def from_binary(data):
return DeleteNodesResult(data)
return DeleteNodesResponse(data)
def _binary_init(self, data):
self.TypeId = NodeId.from_binary(data)
self.ResponseHeader = ResponseHeader.from_binary(data)
length = uatype_Int32.unpack(data.read(4))[0]
array = []
if length != -1:
......@@ -4974,54 +4986,11 @@ class DeleteNodesResult(FrozenClass):
array.append(DiagnosticInfo.from_binary(data))
self.DiagnosticInfos = array
def __str__(self):
return 'DeleteNodesResult(' + 'Results:' + str(self.Results) + ', ' + \
'DiagnosticInfos:' + str(self.DiagnosticInfos) + ')'
__repr__ = __str__
class DeleteNodesResponse(FrozenClass):
'''
Delete one or more nodes from the server address space.
:ivar TypeId:
:vartype TypeId: NodeId
:ivar ResponseHeader:
:vartype ResponseHeader: ResponseHeader
:ivar Parameters:
:vartype Parameters: DeleteNodesResult
'''
def __init__(self, binary=None):
if binary is not None:
self._binary_init(binary)
self._freeze = True
return
self.TypeId = FourByteNodeId(ObjectIds.DeleteNodesResponse_Encoding_DefaultBinary)
self.ResponseHeader = ResponseHeader()
self.Parameters = DeleteNodesResult()
self._freeze = True
def to_binary(self):
packet = []
packet.append(self.TypeId.to_binary())
packet.append(self.ResponseHeader.to_binary())
packet.append(self.Parameters.to_binary())
return b''.join(packet)
@staticmethod
def from_binary(data):
return DeleteNodesResponse(data)
def _binary_init(self, data):
self.TypeId = NodeId.from_binary(data)
self.ResponseHeader = ResponseHeader.from_binary(data)
self.Parameters = DeleteNodesResult.from_binary(data)
def __str__(self):
return 'DeleteNodesResponse(' + 'TypeId:' + str(self.TypeId) + ', ' + \
'ResponseHeader:' + str(self.ResponseHeader) + ', ' + \
'Parameters:' + str(self.Parameters) + ')'
'Results:' + str(self.Results) + ', ' + \
'DiagnosticInfos:' + str(self.DiagnosticInfos) + ')'
__repr__ = __str__
......
......@@ -11,10 +11,10 @@ import xml.etree.ElementTree as ET
NeedOverride = []
NeedConstructor = []#["RelativePathElement", "ReadValueId", "OpenSecureChannelParameters", "UserIdentityToken", "RequestHeader", "ResponseHeader", "ReadParameters", "UserIdentityToken", "BrowseDescription", "ReferenceDescription", "CreateSubscriptionParameters", "PublishResult", "NotificationMessage", "SetPublishingModeParameters"]
IgnoredEnums = []#["IdType", "NodeIdType"]
#we want to implement som struct by hand, to make better interface or simply because they are too complicated
#we want to implement som struct by hand, to make better interface or simply because they are too complicated
IgnoredStructs = []#["NodeId", "ExpandedNodeId", "Variant", "QualifiedName", "DataValue", "LocalizedText"]#, "ExtensionObject"]
#by default we split requests and respons in header and parameters, but some are so simple we do not split them
NoSplitStruct = ["GetEndpointsResponse", "CloseSessionRequest", "AddNodesResponse", "BrowseResponse", "HistoryReadResponse", "HistoryUpdateResponse", "RegisterServerResponse", "CloseSecureChannelRequest", "CloseSecureChannelResponse", "CloseSessionRequest", "CloseSessionResponse", "UnregisterNodesResponse", "MonitoredItemModifyRequest", "MonitoredItemsCreateRequest", "ReadResponse", "WriteResponse", "TranslateBrowsePathsToNodeIdsResponse", "DeleteSubscriptionsResponse", "DeleteMonitoredItemsResponse", "CreateMonitoredItemsResponse", "ServiceFault", "AddReferencesRequest", "AddReferencesResponse", "ModifyMonitoredItemsResponse", "RepublishResponse", "CallResponse", "FindServersResponse", "RegisterServerRequest", "RegisterServer2Response"]
NoSplitStruct = ["GetEndpointsResponse", "CloseSessionRequest", "AddNodesResponse", "DeleteNodesResponse", "BrowseResponse", "HistoryReadResponse", "HistoryUpdateResponse", "RegisterServerResponse", "CloseSecureChannelRequest", "CloseSecureChannelResponse", "CloseSessionRequest", "CloseSessionResponse", "UnregisterNodesResponse", "MonitoredItemModifyRequest", "MonitoredItemsCreateRequest", "ReadResponse", "WriteResponse", "TranslateBrowsePathsToNodeIdsResponse", "DeleteSubscriptionsResponse", "DeleteMonitoredItemsResponse", "CreateMonitoredItemsResponse", "ServiceFault", "AddReferencesRequest", "AddReferencesResponse", "ModifyMonitoredItemsResponse", "RepublishResponse", "CallResponse", "FindServersResponse", "RegisterServerRequest", "RegisterServer2Response"]
#structs that end with Request or Response but are not
NotRequest = ["MonitoredItemCreateRequest", "MonitoredItemModifyRequest", "CallMethodRequest"]
OverrideTypes = {}#AttributeId": "AttributeID", "ResultMask": "BrowseResultMask", "NodeClassMask": "NodeClass", "AccessLevel": "VariableAccessLevel", "UserAccessLevel": "VariableAccessLevel", "NotificationData": "NotificationData"}
......@@ -54,7 +54,7 @@ class Struct(object):
if f.name == name:
return f
raise Exception("field not found: " + name)
def __str__(self):
return "Struct {}:{}".format(self.name, self.basetype)
......@@ -69,7 +69,7 @@ class Field(object):
self.sourcetype = None
self.switchfield = None
self.switchvalue = None
self.bitlength = 1
self.bitlength = 1
def __str__(self):
return "Field {}({})".format(self.name, self.uatype)
......@@ -213,7 +213,7 @@ def remove_duplicates(model):
names.append(field.name)
fields.append(field)
struct.fields = fields
def add_encoding_field(model):
for struct in model.structs:
newfields = []
......@@ -226,7 +226,7 @@ def add_encoding_field(model):
b.name = field.name
b.idx = 0
b.container = container
b.length = 6
b.length = 6
idx = b.length
struct.bits[b.name] = b
......@@ -299,10 +299,10 @@ def split_requests(model):
paramstruct = Struct()
if structtype == "Request":
basename = struct.name.replace("Request", "") + "Parameters"
paramstruct.name = basename
paramstruct.name = basename
else:
basename = struct.name.replace("Response", "") + "Result"
paramstruct.name = basename
paramstruct.name = basename
paramstruct.fields = struct.fields[2:]
paramstruct.bits = struct.bits
......@@ -311,8 +311,8 @@ def split_requests(model):
structs.append(paramstruct)
typeid = Field()
typeid.name = "Parameters"
typeid.uatype = paramstruct.name
typeid.name = "Parameters"
typeid.uatype = paramstruct.name
struct.fields.append(typeid)
structs.append(struct)
model.structs = structs
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment