Skip to content

Commit c161ab3

Browse files
acolombaf-silva
andauthored
DS402: Allow handling the operation mode via PDO. (#257)
* pdo: Document relation between start() and the period attribute. * pdo: Add a property to check if period updates are transmitted. * p402: Skip sending the controlword RPDO if configured with a period. Transmitting the RPDO only makes sense if the value takes effect immediately. If there is already a cyclic task configured, or the transmission type configuration requires a SYNC to be sent for the change to apply, there is no sense in sending the PDO automatically in the controlword setter. * p402: Use RPDO to set the operation mode if possible. Fall back to the previous behavior using SDO if the relevant object 0x6060 is not mapped to an RPDO. * p402: Use TPDO to get the operation mode if possible. Fall back to the previous behavior using SDO if the relevant object 0x6061 is not mapped to a TPDO. The property getter still blocks until an up-to-date value was received, by waiting for the respective TPDO up to a configurable timeout of 0.2 seconds by default. If the TPDO does not look like it will be transmitted regularly (from its is_periodic property), the method will not block and just return the last received TPDO's value. A lookup cache tpdo_pointers is added to keep track of the needed pdo.Map instance, analog to the rpdo_pointers. * p402: Improve documentation on PDO tracking dicts. Consistently use empty dict literal for initialization. Provide more useful comments about the expected contents. * p402: Check PDO configuration for the Operation Mode objects. Switching operation modes for several drives simultaneously must be done via PDO. There is still a fallback mechanism via SDO, but a warning should be issued when that is about to be used. Co-authored-by: André Filipe Silva <[email protected]>
1 parent 1a0ebd7 commit c161ab3

File tree

2 files changed

+55
-7
lines changed

2 files changed

+55
-7
lines changed

canopen/pdo/base.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,8 @@ def __init__(self, pdo_node, com_record, map_array):
186186
self.data = bytearray()
187187
#: Timestamp of last received message
188188
self.timestamp = None
189-
#: Period of receive message transmission in seconds
189+
#: Period of receive message transmission in seconds.
190+
#: Set explicitly or using the :meth:`start()` method.
190191
self.period = None
191192
self.callbacks = []
192193
self.receive_condition = threading.Condition()
@@ -273,6 +274,23 @@ def name(self):
273274
node_id = self.cob_id & 0x7F
274275
return "%sPDO%d_node%d" % (direction, map_id, node_id)
275276

277+
@property
278+
def is_periodic(self):
279+
"""Indicate whether PDO updates will be transferred regularly.
280+
281+
If some external mechanism is used to transmit the PDO regularly, its cycle time
282+
should be written to the :attr:`period` member for this property to work.
283+
"""
284+
if self.period is not None:
285+
# Configured from start() or externally
286+
return True
287+
elif self.trans_type is not None and self.trans_type <= 0xF0:
288+
# TPDOs will be transmitted on SYNC, RPDOs need a SYNC to apply, so
289+
# assume that the SYNC service is active.
290+
return True
291+
# Unknown transmission type, assume non-periodic
292+
return False
293+
276294
def on_message(self, can_id, data, timestamp):
277295
is_transmitting = self._task is not None
278296
if can_id == self.cob_id and not is_transmitting:
@@ -459,7 +477,10 @@ def transmit(self):
459477
def start(self, period=None):
460478
"""Start periodic transmission of message in a background thread.
461479
462-
:param float period: Transmission period in seconds
480+
:param float period:
481+
Transmission period in seconds. Can be omitted if :attr:`period` has been set
482+
on the object before.
483+
:raises ValueError: When neither the argument nor the :attr:`period` is given.
463484
"""
464485
# Stop an already running transmission if we have one, otherwise we
465486
# overwrite the reference and can lose our handle to shut it down

canopen/profiles/p402.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -202,13 +202,15 @@ class BaseNode402(RemoteNode):
202202
TIMEOUT_SWITCH_OP_MODE = 0.5 # seconds
203203
TIMEOUT_SWITCH_STATE_FINAL = 0.8 # seconds
204204
TIMEOUT_SWITCH_STATE_SINGLE = 0.4 # seconds
205+
TIMEOUT_CHECK_TPDO = 0.2 # seconds
205206
INTERVAL_CHECK_STATE = 0.01 # seconds
206207
TIMEOUT_HOMING_DEFAULT = 30 # seconds
207208

208209
def __init__(self, node_id, object_dictionary):
209210
super(BaseNode402, self).__init__(node_id, object_dictionary)
210-
self.tpdo_values = dict() # { index: TPDO_value }
211-
self.rpdo_pointers = dict() # { index: RPDO_pointer }
211+
self.tpdo_values = {} # { index: value from last received TPDO }
212+
self.tpdo_pointers = {} # { index: pdo.Map instance }
213+
self.rpdo_pointers = {} # { index: pdo.Map instance }
212214

213215
def setup_402_state_machine(self):
214216
"""Configure the state machine by searching for a TPDO that has the StatusWord mapped.
@@ -220,6 +222,7 @@ def setup_402_state_machine(self):
220222
self.setup_pdos()
221223
self._check_controlword_configured()
222224
self._check_statusword_configured()
225+
self._check_op_mode_configured()
223226
self.nmt.state = 'OPERATIONAL'
224227
self.state = 'SWITCH ON DISABLED' # Why change state?
225228

@@ -236,6 +239,7 @@ def _init_tpdo_values(self):
236239
logger.debug('Configured TPDO: {0}'.format(obj.index))
237240
if obj.index not in self.tpdo_values:
238241
self.tpdo_values[obj.index] = 0
242+
self.tpdo_pointers[obj.index] = obj
239243

240244
def _init_rpdo_pointers(self):
241245
# If RPDOs have overlapping indecies, rpdo_pointers will point to
@@ -259,6 +263,16 @@ def _check_statusword_configured(self):
259263
"Statusword not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
260264
self.id))
261265

266+
def _check_op_mode_configured(self):
267+
if 0x6060 not in self.rpdo_pointers: # Operation Mode
268+
logger.warning(
269+
"Operation Mode not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
270+
self.id))
271+
if 0x6061 not in self.tpdo_values: # Operation Mode Display
272+
logger.warning(
273+
"Operation Mode Display not configured in node {0}'s PDOs. Using SDOs can cause slow performance.".format(
274+
self.id))
275+
262276
def reset_from_fault(self):
263277
"""Reset node from fault and set it to Operation Enable state."""
264278
if self.state == 'FAULT':
@@ -335,7 +349,7 @@ def homing(self, timeout=TIMEOUT_HOMING_DEFAULT):
335349
def op_mode(self):
336350
"""The node's Operation Mode stored in the object 0x6061.
337351
338-
Uses SDO to access the current value. The modes are passed as one of the
352+
Uses SDO or PDO to access the current value. The modes are passed as one of the
339353
following strings:
340354
341355
- 'NO MODE'
@@ -354,7 +368,18 @@ def op_mode(self):
354368
:raises TypeError: When setting a mode not advertised as supported by the node.
355369
:raises RuntimeError: If the switch is not confirmed within the configured timeout.
356370
"""
357-
return OperationMode.CODE2NAME[self.sdo[0x6061].raw]
371+
try:
372+
pdo = self.tpdo_pointers[0x6061].pdo_parent
373+
if pdo.is_periodic:
374+
timestamp = pdo.wait_for_reception(timeout=self.TIMEOUT_CHECK_TPDO)
375+
if timestamp is None:
376+
raise RuntimeError("Timeout getting node {0}'s mode of operation.".format(
377+
self.id))
378+
code = self.tpdo_values[0x6061]
379+
except KeyError:
380+
logger.warning('The object 0x6061 is not a configured TPDO, fallback to SDO')
381+
code = self.sdo[0x6061].raw
382+
return OperationMode.CODE2NAME[code]
358383

359384
@op_mode.setter
360385
def op_mode(self, mode):
@@ -435,7 +460,9 @@ def controlword(self):
435460
def controlword(self, value):
436461
if 0x6040 in self.rpdo_pointers:
437462
self.rpdo_pointers[0x6040].raw = value
438-
self.rpdo_pointers[0x6040].pdo_parent.transmit()
463+
pdo = self.rpdo_pointers[0x6040].pdo_parent
464+
if not pdo.is_periodic:
465+
pdo.transmit()
439466
else:
440467
self.sdo[0x6040].raw = value
441468

0 commit comments

Comments
 (0)