Skip to content
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

Implement the remaining canopen datatypes #440

Merged

Conversation

sveinse
Copy link
Collaborator

@sveinse sveinse commented May 19, 2024

  • Add datatype defs for the canopen standard types
  • Rename datatypes_24bit.py to datatypes_struct.py
  • Replace Unsigned24 and Interger24 by generic UnsignedN and IntegerN respectively
  • Add EDS-file containing all datatypes
  • Added tests for encoding and decoding all datatypes
  • Added tests for SDO uploads of all datatypes

Related to #436 and #439. This issue will close #437. Closes #320

* Add datatype defs for the canopen standard types
* Rename datatypes_24bit.py to datatypes_struct.py
* Replace Unsigned24 and Interger24 by generic UnsignedN and IntegerN respectively
* Add EDS-file containing all datatypes
* Added tests for encoding and decoding all datatypes
* Added tests for SDO uploads of all datatypes
@sveinse
Copy link
Collaborator Author

sveinse commented May 19, 2024

There are some notable comments for this PR.

  1. The PR currently fails because of the issue in Inconsistent data truncation of unsupported data types in SDO upload #436. It fails because this PR contains a test that is affected by it. So I need to find a resolution to it before this PR can be finished.

  2. I've taken the liberty to remove Unsigned24 and Integer24 and replace them by new generic classes UnsignedN and IntegerN. I grepped for it and it doesn't seem to be used anywhere else.

  3. The file datatypes_24bit.py to datatypes_struct.py since the name is more generic.

@sveinse
Copy link
Collaborator Author

sveinse commented May 19, 2024

BTW: The following code asks if 'utf-16-le' is correct for UNICODE_STRING. The canopen standard doesn't define any encoding variant to use, except that it should be 16-bit glyphs. I think its a good assumption and choice to use utf-16-le here.

elif self.data_type == UNICODE_STRING:
# Is this correct?
return data.rstrip(b"\x00").decode("utf_16_le", errors="ignore")

Copy link
Collaborator

@acolomb acolomb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good overall, with some outstanding issues noted below.

I haven't checked the tests thoroughly, but trusting in your diligence that they're correct.

canopen/objectdictionary/__init__.py Outdated Show resolved Hide resolved
canopen/objectdictionary/__init__.py Outdated Show resolved Hide resolved
canopen/objectdictionary/__init__.py Outdated Show resolved Hide resolved
canopen/objectdictionary/__init__.py Outdated Show resolved Hide resolved
canopen/objectdictionary/datatypes_struct.py Outdated Show resolved Hide resolved
canopen/objectdictionary/datatypes_struct.py Outdated Show resolved Hide resolved
canopen/objectdictionary/datatypes_struct.py Outdated Show resolved Hide resolved
test/all_datatypes.eds Outdated Show resolved Hide resolved
test/test_sdo.py Outdated Show resolved Hide resolved
test/test_sdo.py Outdated Show resolved Hide resolved
@sveinse
Copy link
Collaborator Author

sveinse commented May 23, 2024

Comments updated

* Move datatypes_struct.py classes to datatypes.py
* Make UnsignedN and IntegerN more pythonic
* Add comments to decode_raw
* Remove commens from encode_raw
* Rename all_datatypes.eds to datatypes.eds
* Updated test and added comments
@sveinse
Copy link
Collaborator Author

sveinse commented May 23, 2024

Updates pushed. There are currently two open issues

Copy link
Collaborator

@acolomb acolomb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for considering the previous feedback. Some styling nit-picks remain, and an actual fix of wrong numeric data type constants.

Regarding the 24-bit compatibility classes, I'm okay with dropping them after @raffi-g's feedback.

I've commented on #436 with my current understanding of the problem, and a possibly easy fix. Should be able to get that squared away soon.

canopen/objectdictionary/datatypes.py Outdated Show resolved Hide resolved
canopen/objectdictionary/__init__.py Show resolved Hide resolved
canopen/objectdictionary/__init__.py Outdated Show resolved Hide resolved
canopen/objectdictionary/datatypes.py Outdated Show resolved Hide resolved
canopen/objectdictionary/datatypes.py Outdated Show resolved Hide resolved
test/test_sdo.py Outdated Show resolved Hide resolved
@sveinse
Copy link
Collaborator Author

sveinse commented May 27, 2024

Should we disable the two failing tests and make a new PR from the result of the discussions in #436? This way we can complete this PR and focus on solving the other.

FAILED test/test_sdo.py::TestSDOClientDatatypes::test_unknown_datatype112 - AssertionError: b'\xb2' != b'\xb2\x01 \x02\x91\x12\x03\x19!p\xfe\xfd\xfc\xfb'
FAILED test/test_sdo.py::TestSDOClientDatatypes::test_unknown_datatype32 - AssertionError: b'\xfe' != b'\xfe\xfd\xfc\xfb'

@sveinse sveinse mentioned this pull request May 27, 2024
@sveinse
Copy link
Collaborator Author

sveinse commented May 27, 2024

Please disregard the "Proposed fix for..." reference above. It was a hasty mistake.

@acolomb
Copy link
Collaborator

acolomb commented Jun 3, 2024

Should we disable the two failing tests and make a new PR [...]?

Testing the solution to #436 requires the same tests. Ideally we'd have one commit where they fail, then the fix. And finally squash both when merging. I guess disabling with a FIXME is appropriate here, since work on the other issue is ongoing and foreseeable to fix them soon.

INTEGER_TYPES = SIGNED_TYPES + UNSIGNED_TYPES
FLOAT_TYPES = (REAL32, REAL64)
NUMBER_TYPES = INTEGER_TYPES + FLOAT_TYPES
DATA_TYPES = (VISIBLE_STRING, OCTET_STRING, UNICODE_STRING, DOMAIN)


class UnsignedN:
Copy link
Collaborator

@acolomb acolomb Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that using these classes (just as the previous 24-bit only variants) would break with PDO that are not aligned to full bytes. That's because the code there actually examines the .format attribute, which is only available on the real Struct class:

if od_struct.format.islower() and (1 << (self.length - 1)) < data:

Let's add a read-only str property to forward .format to the self.struct.format instance variable. Or should we instead just derive these classes from struct.Struct?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What exactly is this test doing? It tests that the lower-case of the struct format string is not empty? So is it then a method to determine all types that are struct native, i.e. not 24, 48, 56 and so on? This code is very strange to me. The comment sais "Check if the variable is signed and if the data is negative prepend signedness". I think I need some enlightenment here.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test hinges on the implementation detail that the struct mini-language uses uppercase codes (B, H, L, Q) for unsigned and lowercase (b, h, l, q) for signed types. It then goes on to check whether the value is in the upper half of the representable number space (with the specified number of bits), which indicates that it was a negative number being truncated. In that case, padding with 1-bits is added to the left to make it a truly negative integer (in two's complement) after converting back to the full-width integer type.

This is needed for partially mapped signed integer variables. Judging from the name, the STRUCT_TYPES listing was meant to indicate we do have a struct-based en- / decoder available, thus it can be assumed that the uppercase / lowercase distinction works.

I think the easiest way to fix this is to actually inherit from struct.Struct in your generic integer classes, so that we do have access to a real .format attribute again. Inheritance instead of composition, as an OOP specialist would put it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahhh. It sais islower() not lower()... Sorry, I misread it.

I agree, let's implement some upper case letter for unsigned and lower case for signed. I think simply proxying format to self.struct.format in the UnsignedN and SignedN object will suffice.

Copy link
Collaborator

@acolomb acolomb Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really prefer the inheritance approach, as it allows us to also type-annotate the STRUCT_TYPES mapping as dict[int, struct.Struct] and we don't need to list the IntegerN and UnsignedN classes anywhere. Should also simplify these classes a bit.

Should I take a stab at converting them? For example:

class UnsignedN(struct.Struct):
    """Packing and unpacking unsigned integers of arbitrary width, like struct.Struct.

    The width must be a multiple of 8 and must be between 8 and 64.
    """
    def __init__(self, width: int):
        self.width = width
        if width % 8 != 0:
            raise ValueError("Width must be a multiple of 8")
        if width <= 0 or width > 64:
            raise ValueError("Invalid width for UnsignedN")
        elif width <= 8:
            fmt = "B"
        elif width <= 16:
            fmt = "<H"
        elif width <= 32:
            fmt = "<L"
        else:
            fmt = "<Q"
        super().__init__(fmt)

    def unpack(self, buffer):
        return super().unpack(buffer + b'\x00' * (super().size - self.size))

    def pack(self, *v):
        return super().pack(*v)[:self.size]

    @property
    def size(self) -> int:
        return self.width // 8

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

struct.Struct is a base type; you can safely inherit from it. Builtin types and stdlib types that are not safe to inherit from are not base types (or should not be base types). See also:

I see now that that page needs some care (yes, the triple question marks...)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, it never crossed my mind that a Python class in the stdlib could NOT be subclassed. If such a thing exists, I hope there is a big fat runtime or compiler warning, besides being clearly stated in the documentation.

Copy link
Contributor

@erlend-aasland erlend-aasland Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll get a TypeError:

Python 3.13.0b1 (v3.13.0b1:2268289a47, May  8 2024, 06:41:53) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import datetime
>>> class C(datetime.timezone): ...
... 
Traceback (most recent call last):
  File "<python-input-1>", line 1, in <module>
    class C(datetime.timezone): ...
TypeError: type 'datetime.timezone' is not an acceptable base type
>>> 

For most user-visible types, there is a notice of some kind in the docs:

Wrong link, and more importantly, the docs do not mention it in this case.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway (continuing the digression): most non-base types are not user visible. For example, the _struct.unpack_iter type:

Python 3.13.0b1 (v3.13.0b1:2268289a47, May  8 2024, 06:41:53) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import struct
>>> tp = type(struct.iter_unpack("b", b'x'))
>>> tp.__name__
'unpack_iterator'
>>> class C(tp): ...
... 
Traceback (most recent call last):
  File "<python-input-3>", line 1, in <module>
    class C(tp): ...
TypeError: type '_struct.unpack_iterator' is not an acceptable base type
>>> 

@acolomb
Copy link
Collaborator

acolomb commented Jun 10, 2024

@sveinse I feel pretty confident this is going in the right direction and will work well except for the failure #436, which we should postpone until after a short-term release. But if conflicts are resolved and the failing tests masked out (temporarily), IMHO we could target this for the upcoming release as well. The errors only do manifest under certain conditions, right? So we could at least deliver support for the additional types, even if still buggy in some edge-cases?

WDYT @christiansandberg?

@acolomb acolomb added this to the v2.3.0 milestone Jun 10, 2024
@sveinse
Copy link
Collaborator Author

sveinse commented Jun 10, 2024

@acolomb It would be awesome to have this included in the release.

The failing tests only fail because of the lack of support for custom datatypes as per #436. It would be fine to disable these two tests for now and just state that canopen doesn't support custom types (as it never has).

I'm a bit afk this week, so if you'd like to add the struct proposal, please do so. I believe the PR is opened for maintainer push. Thanks!

@acolomb acolomb merged commit 9df972c into christiansandberg:master Jun 11, 2024
1 check passed
@sveinse sveinse deleted the feature-add-missing-datatypes branch June 11, 2024 21:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Object Dictionary module | UNSIGNED48 | CANOpen Lift 417 IO-Port EDS Request Data Type Additions
5 participants