-
Notifications
You must be signed in to change notification settings - Fork 365
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Server support for read array with index range #636
base: master
Are you sure you want to change the base?
Server support for read array with index range #636
Conversation
Is that working? Nobody is supposed to use address space class directly... |
Well, it works. I honestly don't thoroughly understand the inner of the project, more form the user perspective. However, after debugging and trying to follow the code I have came to the conclusion that when an external read (a read requested issued by a client) lands on the
Now, if you ask me, I wonder why is the Do you think this needs fixing also, or is this intentional and is like so for a reason. Maybe someone can help me in this point.
I am not sure if I will know how to use the tests you have setup, but will give it a try.
I can do that. Moreover, as far as I can see this functionality is also missing for the write operation, maybe can be addressed later in other PR if I can understand the "correct" fix for this. |
with this last changes address_space.py::AttributeService seems that is rendered unused. If this changes are okay maybe it must be removed. |
value = attval.value_callback() | ||
else: | ||
value = attval.value | ||
if not index_range is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if index_range is not None if the advices syntax.
But I am rather sure you could write: if index_range
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
and that test should be ealier, so you do not need to overwrite a local value
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In fact IMO if we don't make the test here (after knowing if the value is read via attval.value_callback
or via attval.value
) it makes the implementation more complex.
The check if just a range is wanted or full array wanted relies on having the full array already, specially because I don't understand when attval.value_callback()
is called and how does it work; as far as I can see it is not used, I couldn't find any location on the code where it is set. If you or someone can confirm that it can be removed, than the code can be simplified.
So I think this implementation is safer, but eventually worst performing (we can make some lambdas for trying to minimize number of copies, but...). In case value_callback
is used, in order to make the check before we would need to change the value_callback
so it could also take a range.
Other option is to sacrifice performance only when the value is obtained via value_callback
and having some code redundancy for range (maybe mitigated with some helper function).
What do you recon to be the best alternative?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@oroulet I have some interest in having this feature fixed 😸
Could you help me getting this PR in shape to get merged?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fix that one: if index_range is not None
is the advices syntax.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you help me getting this PR in shape to get merged?
fix the comments from today and it might be good enough
@@ -696,11 +710,35 @@ def read_attribute_value(self, nodeid, attr): | |||
dv = ua.DataValue(StatusCode_=ua.StatusCode(ua.StatusCodes.BadAttributeIdInvalid)) | |||
return dv | |||
attval = node.attributes[attr] | |||
value = ua.DataValue() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also it looks like the logic is more complicated than necessary. That local variable is useless.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just had a second look. That variable is really completely useless. It is ALWAYS overwritten in one of the next 4 lines
return attval.value | ||
|
||
async def write_attribute_value(self, nodeid, attr, value): | ||
value = attval.value_callback() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just return here insteas of making a local variable
async def write_attribute_value(self, nodeid, attr, value): | ||
value = attval.value_callback() | ||
else: | ||
value = attval.value |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
|
||
Returns: | ||
Tuple[int,int]: Equivalent python list slicing tuple with low and high bounds | ||
""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good method
if len(index_range_list) == 1: | ||
high_idx = int(index_range_list[0])+1 | ||
elif len(index_range_list) == 2: | ||
high_idx = int(index_range_list[1])+1 # python List[n:n] -> empty list, List[n:n+k] -> list of k elements; OPC UA see https://reference.opcfoundation.org/v104/Core/docs/Part4/7.22/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please do nt write so long lines. split it. And also I am sure linters complain about nothaving 2 spaces before #
elif len(index_range_list) == 2: | ||
high_idx = int(index_range_list[1])+1 # python List[n:n] -> empty list, List[n:n+k] -> list of k elements; OPC UA see https://reference.opcfoundation.org/v104/Core/docs/Part4/7.22/ | ||
else: | ||
raise Exception(f'Read with bad index range: {ua_index_range}') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thin ValueError is the correct excpetino to use in this case. Unless UA defines one for such case
return ua.StatusCode(ua.StatusCodes.BadWriteNotSupported) | ||
try: | ||
value = DataValue(Variant(value.Value.Value[index_range[0]:index_range[1]])) | ||
except Exception as ex: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
catching exception is usually a bad idea. In that case there are two things that can happen: the value is not an array or indexes are out of bound. Only catch these two things
await self.aspace.write_attribute_value(nodeid, attr, datavalue) | ||
|
||
def read_attribute_value(self, nodeid, attr=ua.AttributeIds.Value): | ||
if not index_range is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here: if index_range is not None
if not index_range is None: | ||
try: | ||
py_list_index_range = ua_index_range_to_list_slice(index_range) | ||
except Exception as ex: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
probably possible to only catch ValueError here
""" | ||
directly read datavalue of the Attribute | ||
""" | ||
return self.aspace.read_attribute_value(nodeid, attr) | ||
if not index_range is None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here
self.logger.warning(f'Parse index range failed. IndexRange = {index_range}; Ex: {ex}') | ||
return ua.DataValue(StatusCode_=ua.StatusCode(ua.StatusCodes.BadIndexRangeInvalid)) | ||
else: | ||
py_list_index_range = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also that code is exactly the same in both read and write, put it in an internal method
results = [ | ||
self.iserver.read_attribute_value(node_to_read.NodeId, node_to_read.AttributeId, node_to_read.IndexRange) | ||
for node_to_read in params.NodesToRead | ||
] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why did you change passing all arguments to passing them one by one? you are proably taking away some arguments, and the loop was probably anyway inside the read() method in iserver
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. might have been a misundersting, maybe you did all of that to move out the string parsing. your string parsing function is fine, if the UA attribute uses a string we have no choice but pass it dow to address_space.py and do the parsing there if we have to
await self.iserver.callback_service.dispatch(CallbackType.PostRead, | ||
ServerItemCallback(params, results, user)) | ||
return results | ||
|
||
async def history_read(self, params) -> Coroutine: | ||
return await self.iserver.history_manager.read_history(params) | ||
|
||
async def write(self, params): | ||
def check_user_access_to_node_to_write(self, user:User, node_to_write:WriteValue)->bool: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if this is an private method(not used anywhere else) then it should start with underscore
@@ -663,15 +664,15 @@ async def load_enums(self) -> Coroutine: | |||
_logger.warning("Deprecated since spec 1.04, call load_data_type_definitions") | |||
return await load_enums(self) | |||
|
|||
async def write_attribute_value(self, nodeid, datavalue, attr=ua.AttributeIds.Value): | |||
async def write_attribute_value(self, nodeid, datavalue, attr=ua.AttributeIds.Value, index_range:Optional[str]=None)->StatusCode: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, this is supposed to be the super efficitent server side write method, there we should use python indexes, not string that need to be parsed
|
||
def read_attribute_value(self, nodeid, attr=ua.AttributeIds.Value): | ||
def read_attribute_value(self, nodeid, attr=ua.AttributeIds.Value, index_range:Optional[str]=None)->DataValue: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here. string parsing is slow
@@ -292,18 +294,34 @@ def unsubscribe_server_callback(self, event, handle): | |||
""" | |||
self.callback_service.removeListener(event, handle) | |||
|
|||
async def write_attribute_value(self, nodeid, datavalue, attr=ua.AttributeIds.Value): | |||
async def write_attribute_value(self, nodeid, datavalue, attr=ua.AttributeIds.Value, index_range:Optional[str]=None)->StatusCode: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
same here, this is is supposed to be an efficient method. use indexes
@oroulet I took over this task from @jcbastosportela since he doesn't have time for it. |
fixes #635