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

Add support for multiple Cyclic Messages in Task #610

Merged

Conversation

karlding
Copy link
Collaborator

@karlding karlding commented Jun 2, 2019

This adds support for multiple Cyclic Messages in a Cyclic Task. The
default implementation is also changed to provide support for this, by
iterating over the list of Cyclic Messages.

The SocketCAN interface takes advantage of the Linux BCM APIs to do so,
while the IXXAT interface maintains its original behaviour.

This also introduces a new example that illustrates how to use Cyclic
Messages, backed by the SocketCAN interface.

Fixes #606

self.message = message
self.can_id = message.arbitration_id
self.arbitration_id = message.arbitration_id
if not isinstance(messages, list):
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

TBH I'm not really a fan of keeping backwards compatibility and supporting both a List and a Message type. It seems kludgy to muddle the API like that. Plus, a single Message can still be supported by a list of length 1...

Copy link
Collaborator

Choose a reason for hiding this comment

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

No one has really requested this feature for years and neither one of IXXAT, Kvaser, NI-CAN, or Systec interfaces which provide cyclic message transmissions have seen the need for this and won't support it. Therefore I believe it is not justified to force everyone to change their implementations for the very few people that need this. At the very least we must have a deprecation period to allow implementations to adjust. A library that changes its API in backwards incompatible ways too often are not very popular among developers relying on it for critical applications.

Maybe it only has to be converted to a list at one place. Maybe in the send_periodic() method?

Copy link
Owner

Choose a reason for hiding this comment

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

I appreciate how you've (mostly) kept backwards compatibility at the user facing API level - I'm strongly in favor of that.

I agree that it feels kludgy to support both List[Message] and Message throughout the stack and would prefer to try limit that to bus.send_periodic(). The Task returned will still have to adhere to the current api - e.g. ModifiableCyclicTaskABC.modify_data must continue to take a single message (possibly a List[Message] as well).

@codecov
Copy link

codecov bot commented Jun 2, 2019

Codecov Report

Merging #610 into develop will decrease coverage by 0.3%.
The diff coverage is 29.35%.

@@             Coverage Diff             @@
##           develop     #610      +/-   ##
===========================================
- Coverage    63.65%   63.34%   -0.31%     
===========================================
  Files           63       63              
  Lines         5544     5582      +38     
===========================================
+ Hits          3529     3536       +7     
- Misses        2015     2046      +31

@codecov
Copy link

codecov bot commented Jun 2, 2019

Codecov Report

Merging #610 into develop will decrease coverage by 2.76%.
The diff coverage is 78.12%.

@@             Coverage Diff             @@
##           develop     #610      +/-   ##
===========================================
- Coverage    63.14%   60.37%   -2.77%     
===========================================
  Files           66       66              
  Lines         5904     5908       +4     
===========================================
- Hits          3728     3567     -161     
- Misses        2176     2341     +165

Copy link
Collaborator

@christiansandberg christiansandberg left a comment

Choose a reason for hiding this comment

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

Perhaps you could refactor out some of the repeated checks to a utility function.


# Take the Arbitration ID of the first element
self.can_id = messages[0].arbitration_id
self.arbitration_id = messages[0].arbitration_id
Copy link
Collaborator

Choose a reason for hiding this comment

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

Could we perhaps remove one of these now?

while not self.stopped:
# Prevent calling bus.send from multiple threads
with self.lock:
started = time.time()
try:
self.bus.send(self.message)
self.bus.send(self.messages[msg_index])
msg_index = (msg_index + 1) % len(self.messages)
Copy link
Collaborator

Choose a reason for hiding this comment

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

You could move the msg_index increment outside the lock.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Maybe I'm missing something, but why is this lock even necessary? What is this lock intended to protect?

From what I understand, there is a 1:1 mapping between cyclic tasks and threads, so _run is essentially running in its own context for each thread. As such, the thread can get preempted, but nothing should be modifying its context state while it is context switched out.

According to git blame, you were the one that added this locking, so I thought I'd take the opportunity to ask you 😂

If this lock is actually protecting against other threads poking into the context, then I believe the increment still needs to be protected by the lock, since Python doesn't provide atomics.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I think the lock prevents Bus.send(...) from being called concurrently, and not the update of msg_index. This is required since Bus.send(...) is not guaranteed to be thread safe.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, thanks!

def __init__(self, scheduler, msgs, period, duration, resolution):
super().__init__(msgs, period, duration)
# Only supports 1 Message in the group?
if len(self.messages) == 1:
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would probably just do a check in the beginning instead.

if len(self.messages) != 1:
    raise ValueError("IXXAT Interface only supports 1 element")

self._index = ctypes.c_uint32()
_canlib.canSchedulerAddMessage(self._scheduler, self._msg, self._index)
_canlib.canSchedulerStartMessage(self._scheduler, self._index, self._count)
if len(self.messages) == 1:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Don't think we need to check self.messages again since it should not have changed.

body = bytearray()
for message in messages:
self.can_id_with_flags = _add_flags_to_can_id(message)
self.flags = CAN_FD_FRAME if message.is_fd else 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

These two can be set outside the loop to the value of the first message.

ival1 = 0
ival2 = self.period
body += build_can_frame(message)
log.debug("Sending BCM command")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move outside loop.

)
for message in messages:
self.can_id_with_flags = _add_flags_to_can_id(message)
self.flags = CAN_FD_FRAME if message.is_fd else 0
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move outside loop using first entry.

self.can_id_with_flags = _add_flags_to_can_id(message)
self.flags = CAN_FD_FRAME if message.is_fd else 0
body += build_can_frame(message)
log.debug("Sending BCM command")
Copy link
Collaborator

Choose a reason for hiding this comment

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

Move outside loop.

Copy link
Owner

@hardbyte hardbyte left a comment

Choose a reason for hiding this comment

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

Overall I think this is a good addition.

We should improve the docs and add a couple of tests similar to SimpleCyclicSendTaskTest before merging.

can/bus.py Outdated
@@ -174,7 +174,7 @@ def send_periodic(self, msg, period, duration=None, store_task=True):
- :meth:`BusABC.stop_all_periodic_tasks()` is called
- the task's :meth:`CyclicTask.stop()` method is called.

:param can.Message msg:
:param List[can.Message] msgs:
Copy link
Owner

Choose a reason for hiding this comment

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

This should be something like Union[List[can.Message], can.Message], the docstring should be updated too.

def __init__(self, scheduler, msgs, period, duration, resolution):
super().__init__(msgs, period, duration)
if len(self.messages) != 1:
raise ValueError("IXXAT Interface only supports 1 element")
Copy link
Owner

Choose a reason for hiding this comment

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

Suggested change
raise ValueError("IXXAT Interface only supports 1 element")
raise ValueError("IXXAT Interface only supports periodic sending of 1 element")

can/interfaces/socketcan/socketcan.py Outdated Show resolved Hide resolved
can/interfaces/socketcan/socketcan.py Show resolved Hide resolved
examples/cyclic_multiple.py Outdated Show resolved Hide resolved
examples/cyclic_multiple.py Show resolved Hide resolved
self.message = message
self.can_id = message.arbitration_id
self.arbitration_id = message.arbitration_id
if not isinstance(messages, list):
Copy link
Owner

Choose a reason for hiding this comment

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

I appreciate how you've (mostly) kept backwards compatibility at the user facing API level - I'm strongly in favor of that.

I agree that it feels kludgy to support both List[Message] and Message throughout the stack and would prefer to try limit that to bus.send_periodic(). The Task returned will still have to adhere to the current api - e.g. ModifiableCyclicTaskABC.modify_data must continue to take a single message (possibly a List[Message] as well).

examples/cyclic_multiple.py Show resolved Hide resolved
can/broadcastmanager.py Outdated Show resolved Hide resolved
can/broadcastmanager.py Outdated Show resolved Hide resolved
Copy link
Collaborator

@felixdivo felixdivo left a comment

Choose a reason for hiding this comment

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

Sorry, I don't have time for a full review but am okay with changing the API to use lists.

@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from 46fd898 to 21eec39 Compare June 4, 2019 08:00
can/bus.py Show resolved Hide resolved
Note: The number of new cyclic messages to be sent must be equal to the
original number of messages originally specified for this task.

:param Union[List[can.Message], tuple(can.Message), can.Message] messages:
Copy link
Owner

Choose a reason for hiding this comment

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

nit: I'd do this with typing.Sequence: Union[Sequence[can.Message], can.Message]

Copy link
Collaborator

@felixdivo felixdivo Jun 6, 2019

Choose a reason for hiding this comment

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

Or typing.Iterable instead of Sequence?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@felixdivo correct me if I'm wrong, but I don't think you can get the length of an Iterable without actually performing the iteration and storing the result.

If want to support an Iterable, do we want to deal with buffering potentially unbounded input? Or implement specific bounded iterators for each interface that only iterates over the maximum number of elements the interface task can support, and errors otherwise?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Ah I see. We could wrap it in a list if it's not already a sequence. Implementing something like that ourselves seems overkill. If you want, stay with what you have or a sequence.


return messages

def _check_modified_messages(self, messages):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Not sure this is the write location for this method since it's only ever use by ModifiableCyclicTaskABC.modify_data. Maybe it should be in the ModifiableCyclicTaskABC class or nested inside ModifiableCyclicTaskABC.modify_data.

@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch 4 times, most recently from 900ccca to b12ea74 Compare June 18, 2019 23:11
"""
self.message = message
messages = self._check_and_convert_messages(messages)
messages = self._check_modified_messages(messages)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Minor, but self._check_modified_messages(messages) do not make any modification to messages, so you do not really need to return messages

@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from b12ea74 to f4f2bc7 Compare June 22, 2019 22:54
@felixdivo felixdivo added this to the 4.0 Release milestone Jun 23, 2019
@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from f4f2bc7 to 3ff4476 Compare June 23, 2019 14:57
@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from 3ff4476 to 4f45fd5 Compare June 24, 2019 07:00
Copy link
Collaborator

@christiansandberg christiansandberg left a comment

Choose a reason for hiding this comment

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

Haven’t checked in detail but seems good.

can/interfaces/socketcan/socketcan.py Outdated Show resolved Hide resolved
@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from 4f45fd5 to 7671687 Compare June 28, 2019 04:25
This adds support for multiple Cyclic Messages in a Cyclic Task. The
default implementation is also changed to provide support for this, by
iterating over the list of Cyclic Messages. In order to maintain
backwards compatibility, the periodic APIs now take a CAN Message as
before, in addition to a Sequence of CAN Messages.

The SocketCAN interface takes advantage of the Linux BCM APIs to do so,
while the IXXAT interface maintains its original behaviour.

This also introduces a new example that illustrates how to use Cyclic
Messages, backed by the SocketCAN interface.

We previously tracked the can_id and arbitration_id class members due to
the ongoing deprecation of the can_id Message attribute. Now that can_id
is replaced by arbitration_id, we no longer need this in
CyclicSendTaskABC either. As such, this removes the deprecated can_id
member from the Cyclic Task.

Fixes hardbyte#606
@karlding karlding force-pushed the add_support_for_multiple_cyclic_messages branch from 7671687 to c5cff12 Compare June 28, 2019 06:28
@karlding karlding merged commit 56f153f into hardbyte:develop Jul 10, 2019
@karlding karlding deleted the add_support_for_multiple_cyclic_messages branch July 11, 2019 03:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Incorrect frame re-transmission when arbitration ID is duplicate
5 participants