python-can-j1939 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
j1939/j1939_22.py ADDED
@@ -0,0 +1,845 @@
1
+ from .parameter_group_number import ParameterGroupNumber
2
+ from .message_id import MessageId, FrameFormat
3
+ import logging
4
+ import threading
5
+ import time
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+ class J1939_22:
10
+ class TpControlType:
11
+ RTS = 0 # Destination Specific Request_To_Send
12
+ CTS = 1 # Destination Specific Clear_To_Send
13
+ EOM_STATUS = 2 # Destination Specific or Global Destination End_of_Message Status
14
+ EOM_ACK = 3 # Destination Specific End_of_Message Acknowledge
15
+ BAM = 4 # Global Destination Broadcast Announce Message
16
+ ABORT = 15 # Destination Specific Connection Abort
17
+
18
+ class Adt: # assurance data type
19
+ NO_ADT = 0 # no assurance Data
20
+ MS_CS = 1 # Manufacturer specific cybersecurity assurance data
21
+ MS_FS = 2 # Manufacturer specific functional safety assurance
22
+ MS_COMBINED_CS_FS = 3 # Manufacturer specific combined cybersecurity followed by functional safety assurance
23
+
24
+ class ConnectionAbortReason:
25
+ BUSY = 1 # Already in one or more connection managed sessions and cannot support another
26
+ RESOURCES = 2 # System resources were needed for another task so this connection managed session was terminated
27
+ TIMEOUT = 3 # A timeout occured
28
+ # 4..250 Reserved by SAE
29
+ CTS_WHILE_DT = 4 # according AUTOSAR: CTS messages received when data transfer is in progress
30
+ # 251..255 Per J1939/71 definitions - but there are none?
31
+
32
+ class DataLength:
33
+ TP = 60
34
+ MULTI_PG = 60
35
+
36
+ class Timeout:
37
+ """Timeouts according SAE J1939/22"""
38
+ Tr = 0.200 # Maximum Response Time
39
+ Th = 0.500 # Maximum time, for responder, between transmits of consecutive CTS messages during hold
40
+ T1 = 0.750 # Transport Segment Interval
41
+ T2 = 1.250 # Maximum time, for responder, to receive a DT segment after a CTS - Originator Failure
42
+ T3 = 1.250 # Maximum time, for originator, to receive a CTS after last DT segment - Responder Failure
43
+ T4 = 1.050 # Maximum time, for originator, to receive the next CTS messages since the previous “hold” CTS to hold a connection open
44
+ T5 = 3.000 # Maximum time, for originator, to receive EOMA after sending EOMS
45
+
46
+ class SendBufferState:
47
+ WAITING_CTS = 0 # waiting for CTS
48
+ SENDING_RTS_CTS = 1 # sending rts/cts packages
49
+ SENDING_BAM = 2 # sending broadcast packages
50
+ SENDING_EOM_STATUS = 3 # sending end of message
51
+ WAITING_EOM_ACK = 4 # waiting for end of message acknowledge
52
+ EOM_ACK_RECEIVED = 5 # eom acknowledge received successfully
53
+ TRANSMISSION_FINISHED = 6 # finished, remove buffer
54
+
55
+ class Acknowledgement:
56
+ ACK = 0
57
+ NACK = 1
58
+ AccessDenied = 2
59
+ CannotRespond = 3
60
+
61
+ def __init__(self, send_message, job_thread_wakeup, notify_subscribers, max_cmdt_packets, minimum_tp_rts_cts_dt_interval, minimum_tp_bam_dt_interval, ecu_is_message_acceptable):
62
+ # Receive buffers
63
+ self._rcv_buffer = {}
64
+ # Send buffers
65
+ self._snd_buffer = {}
66
+ # Multi-PG Send buffers
67
+ self._multi_pg_snd_buffer = {}
68
+
69
+ # List of ControllerApplication
70
+ self._cas = []
71
+
72
+ self._LUT_FD_DLC = []
73
+ for i in range(9): self._LUT_FD_DLC.append(i)
74
+ for _ in range(4): self._LUT_FD_DLC.append(12)
75
+ for _ in range(4): self._LUT_FD_DLC.append(16)
76
+ for _ in range(4): self._LUT_FD_DLC.append(20)
77
+ for _ in range(4): self._LUT_FD_DLC.append(24)
78
+ for _ in range(8): self._LUT_FD_DLC.append(32)
79
+ for _ in range(16): self._LUT_FD_DLC.append(48)
80
+ for _ in range(16): self._LUT_FD_DLC.append(64)
81
+
82
+ # minimum time between two tp rts/cts dt frames, not necessary for standard conforming applications,
83
+ # (they would use RTS/CTS flow control), but helps to talk to others without patching the library
84
+ self._minimum_tp_rts_cts_dt_interval = minimum_tp_rts_cts_dt_interval
85
+
86
+ # minimum time between two tp bam dt frames, inital value is 10ms
87
+ # specified time range in j1939-22: 10-200ms
88
+ if minimum_tp_bam_dt_interval == None:
89
+ self._minimum_tp_bam_dt_interval = 0.010
90
+ else:
91
+ self._minimum_tp_bam_dt_interval = minimum_tp_bam_dt_interval
92
+
93
+ # Up to 4 concurrent BAM sessions per originator address are allowed
94
+ self.__bam_session_list = [True] * 4
95
+
96
+ # Up to 8 concurrent RTS/CTS sessions per originator and responder address pair are allowed.
97
+ self.__rts_cts_session_list = [True] * 8
98
+
99
+ # number of packets that can be sent/received with CMDT (Connection Mode Data Transfer)
100
+ self._max_cmdt_packets = max_cmdt_packets
101
+
102
+ # Lock protecting _rcv_buffer, _snd_buffer, and _multi_pg_snd_buffer — accessed from
103
+ # both the Notifier thread (notify/process_tp_*) and the protocol job thread (async_job_thread).
104
+ self._buffer_lock = threading.Lock()
105
+
106
+ self.__job_thread_wakeup = job_thread_wakeup
107
+ self.__send_message = send_message
108
+ self.__notify_subscribers = notify_subscribers
109
+ self.__ecu_is_message_acceptable = ecu_is_message_acceptable
110
+
111
+ def add_ca(self, ca):
112
+ self._cas.append(ca)
113
+
114
+ def remove_ca(self, device_address):
115
+ for ca in self._cas:
116
+ if device_address == ca._device_address_preferred:
117
+ self._cas.remove(ca)
118
+ return True
119
+ return False
120
+
121
+ def _buffer_hash(self, session_num, src_address, dest_address):
122
+ """Calculates a hash value for the given address pair
123
+
124
+ :param src_address:
125
+ The Source-Address the connection should bound to.
126
+ :param dest_address:
127
+ The Destination-Address the connection should bound to.
128
+
129
+ :return:
130
+ The calculated hash value.
131
+
132
+ :rtype: int
133
+ """
134
+ return ((session_num & 0xF) << 16) | ((src_address & 0xFF) << 8) | (dest_address & 0xFF)
135
+
136
+ def _buffer_hash_mpg(self, frame_format, msg_counter, src_address, dest_address):
137
+ """Calculates a hash value for the given multi-pg arguments
138
+
139
+ :param frame_format:
140
+ The frame-format (FBFF, FEFF) the connection should bound to.
141
+ :param msg_counter:
142
+ The message counter the connection should bound to.
143
+ :param src_address:
144
+ The Source-Address the connection should bound to.
145
+ :param dest_address:
146
+ The Destination-Address the connection should bound to.
147
+
148
+ :return:
149
+ The calculated hash value.
150
+
151
+ :rtype: int
152
+ """
153
+ return ((frame_format & 0xFF) << 24) | ((msg_counter & 0xFF) << 16) | ((src_address & 0xFF) << 8) | (dest_address & 0xFF)
154
+
155
+ def _buffer_unhash(self, hash):
156
+ """Calculates session-number, source-address and destination-address for the given hash value
157
+
158
+ :param hash:
159
+ The hash to be unhased
160
+
161
+ :return:
162
+ The session-number, source-address and destination-address
163
+ """
164
+ return ((hash >> 16) & 0xFF), ((hash >> 8) & 0xFF), (hash & 0xFF)
165
+
166
+ def _buffer_unhash_mpg(self, hash):
167
+ """Calculates frame_format, msg_counter, source-address and destination-address for the given hash value
168
+
169
+ :param hash:
170
+ The hash to be unhased
171
+
172
+ :return:
173
+ The session-number, source-address and destination-address
174
+ """
175
+ return ((hash >> 24) & 0xFF), ((hash >> 16) & 0xFF), ((hash >> 8) & 0xFF), (hash & 0xFF)
176
+
177
+ def __get_bam_session(self):
178
+ for idx, i in enumerate(self.__bam_session_list):
179
+ if i == True:
180
+ self.__bam_session_list[idx] = False
181
+ return idx
182
+ return None
183
+
184
+ def __put_bam_session(self, session):
185
+ self.__bam_session_list[session] = True
186
+
187
+ def __get_rts_cts_session(self):
188
+ for idx, i in enumerate(self.__rts_cts_session_list):
189
+ if i == True:
190
+ self.__rts_cts_session_list[idx] = False
191
+ return idx
192
+ return None
193
+
194
+ def __put_rts_cts_session(self, session):
195
+ self.__rts_cts_session_list[session] = True
196
+
197
+ def send_pgn(self, data_page, pdu_format, pdu_specific, priority, src_address, data, time_limit, frame_format, tos=2, trailer_format=0):
198
+ pgn = ParameterGroupNumber(data_page, pdu_format, pdu_specific)
199
+ data_length = len(data)
200
+
201
+ if data_length <= self.DataLength.TP:
202
+ if (tos != 2) or (trailer_format != 0):
203
+ print('currently "SAE J1939 with no assurance data" trailer format supported only')
204
+
205
+ if pgn.is_pdu1_format:
206
+ cpgn = pgn.value & 0xFFF00
207
+ dst_address = pdu_specific
208
+ else:
209
+ cpgn = pgn.value
210
+ dst_address = ParameterGroupNumber.Address.GLOBAL
211
+
212
+ if (frame_format==FrameFormat.FBFF):
213
+ priority = 0
214
+ if (dst_address!=ParameterGroupNumber.Address.GLOBAL):
215
+ logger.info('FBFF message must be a broadcast type')
216
+ return False
217
+
218
+ # create header dict
219
+ cpg = {'priority': (priority & 0x7), 'tos': (tos & 0x7), 'tf': (trailer_format & 0x7), 'cpgn': (cpgn & 0x3FFFF), 'data_length': data_length, 'data': data.copy()}
220
+
221
+ # send immediately
222
+ if time_limit == 0:
223
+ self.__send_multi_pg(frame_format, [cpg], src_address, dst_address)
224
+ else:
225
+ session = 0
226
+ deadline = time.monotonic() + time_limit
227
+ with self._buffer_lock:
228
+ while True:
229
+ hash = self._buffer_hash_mpg(frame_format, session, src_address, dst_address)
230
+ if hash not in self._multi_pg_snd_buffer:
231
+ self._multi_pg_snd_buffer[hash] = {'deadline': deadline, 'cpg': [cpg], 'fill_level': 4 + data_length}
232
+ break
233
+ elif (self._multi_pg_snd_buffer[hash]['fill_level'] <= (self.DataLength.TP - data_length)):
234
+ # update fill level
235
+ self._multi_pg_snd_buffer[hash]['fill_level'] += 4 + data_length
236
+ # update deadline
237
+ if self._multi_pg_snd_buffer[hash]['deadline'] > deadline:
238
+ self._multi_pg_snd_buffer[hash]['deadline'] = deadline
239
+ # append c-pg
240
+ self._multi_pg_snd_buffer[hash]['cpg'].append(cpg)
241
+ break
242
+ else:
243
+ # trigger sending
244
+ self._multi_pg_snd_buffer[hash]['deadline'] = time.monotonic()
245
+ self.__job_thread_wakeup()
246
+ # get next buffer
247
+ session += 1
248
+ else:
249
+ # if the PF is between 0 and 239, the message is destination dependent when pdu_specific != 255
250
+ # if the PF is between 240 and 255, the message can only be broadcast
251
+ if (pdu_specific == ParameterGroupNumber.Address.GLOBAL) or ParameterGroupNumber(0, pdu_format, pdu_specific).is_pdu2_format:
252
+ dest_address = ParameterGroupNumber.Address.GLOBAL
253
+ session_num = self.__get_bam_session()
254
+ if session_num == None:
255
+ #print('bam session not available')
256
+ return False
257
+ else:
258
+ dest_address = pdu_specific
259
+ session_num = self.__get_rts_cts_session()
260
+ if session_num == None:
261
+ #print('rts/cts session not available')
262
+ return False
263
+
264
+ # init sequence
265
+ buffer_hash = self._buffer_hash(session_num, src_address, dest_address)
266
+
267
+ message_size = data_length
268
+ num_segments = int(message_size / self.DataLength.TP ) + ((message_size % self.DataLength.TP ) != 0)
269
+
270
+ # set default priority
271
+ if priority == None: priority = 7
272
+
273
+ # get chunks from data
274
+ chunk_size = self.DataLength.TP
275
+ data_list = [list(data[i:i + chunk_size]) for i in range(0, data_length, chunk_size)]
276
+
277
+ # if the PF is between 240 and 255, the message can only be broadcast
278
+ if dest_address == ParameterGroupNumber.Address.GLOBAL:
279
+
280
+ # send BAM
281
+ self.__send_tp_bam(priority, src_address, session_num, pgn.value, message_size, num_segments)
282
+
283
+ # init new buffer for this connection
284
+ with self._buffer_lock:
285
+ self._snd_buffer[buffer_hash] = {
286
+ 'pgn': pgn.value,
287
+ 'priority': priority,
288
+ 'session': session_num,
289
+ 'message_size': message_size,
290
+ 'num_segments': num_segments,
291
+ 'data': data_list,
292
+ 'state': self.SendBufferState.SENDING_BAM,
293
+ 'deadline': time.monotonic() + self._minimum_tp_bam_dt_interval,
294
+ 'src_address' : src_address,
295
+ 'dest_address' : ParameterGroupNumber.Address.GLOBAL,
296
+ 'next_packet_to_send' : 0,
297
+ }
298
+ else:
299
+ # send RTS/CTS
300
+ pgn.pdu_specific = 0 # this is 0 for peer-to-peer transfer
301
+ # init new buffer for this connection
302
+ with self._buffer_lock:
303
+ self._snd_buffer[buffer_hash] = {
304
+ 'pgn': pgn.value,
305
+ 'priority': priority,
306
+ 'session': session_num,
307
+ 'message_size': message_size,
308
+ 'num_segments': num_segments,
309
+ 'data': data_list,
310
+ 'state': self.SendBufferState.WAITING_CTS,
311
+ 'deadline': time.monotonic() + self.Timeout.T3,
312
+ 'src_address' : src_address,
313
+ 'dest_address' : pdu_specific,
314
+ 'next_packet_to_send' : 0,
315
+ 'next_wait_on_cts': 0,
316
+ }
317
+ self.__send_tp_rts(priority, src_address, pdu_specific, session_num, pgn.value, message_size, num_segments, min(self._max_cmdt_packets, num_segments))
318
+
319
+ self.__job_thread_wakeup()
320
+
321
+ return True
322
+
323
+ def __send_multi_pg(self, frame_format, cpg_list, src_address, dst_address):
324
+ # deadline reached
325
+ priority = 7
326
+ data = []
327
+ for cpg in cpg_list:
328
+ priority = min(cpg['priority'], priority)
329
+ data.append( (cpg['tos'] << 5) | (cpg['tf'] << 2) | ((cpg['cpgn'] >> 16) & 0x3) )
330
+ data.append( ((cpg['cpgn'] >> 8) & 0xFF) )
331
+ data.append( (cpg['cpgn'] & 0xFF) )
332
+ data.append( cpg['data_length'] )
333
+ data.extend( cpg['data'])
334
+
335
+ # padding
336
+ next_valid_fd_length = self._LUT_FD_DLC[len(data)]
337
+ if next_valid_fd_length < 0:
338
+ next_valid_fd_length = 0
339
+
340
+ # padding with service header 0
341
+ padding_cnt = 0
342
+ while len(data)<next_valid_fd_length:
343
+ if padding_cnt < 3:
344
+ data.append(0)
345
+ padding_cnt += 1
346
+ else:
347
+ data.append(0xAA)
348
+
349
+ if frame_format == FrameFormat.FBFF:
350
+ self.__send_message(src_address, False, data, fd_format=True)
351
+ else:
352
+ mid = MessageId(priority=priority,
353
+ parameter_group_number=ParameterGroupNumber.PGN.FEFF_MULTI_PG | (dst_address & 0xFF),
354
+ source_address=src_address)
355
+ self.__send_message(mid.can_id, True, data, fd_format=True)
356
+
357
+
358
+ def async_job_thread(self, now):
359
+
360
+ next_wakeup = now + 5.0 # wakeup in 5 seconds
361
+
362
+ with self._buffer_lock:
363
+ # check receive buffers for timeout
364
+ for bufid in list(self._rcv_buffer):
365
+ buf = self._rcv_buffer[bufid]
366
+ if buf['deadline'] != 0:
367
+ if buf['deadline'] > now:
368
+ if next_wakeup > buf['deadline']:
369
+ next_wakeup = buf['deadline']
370
+ else:
371
+ # deadline reached
372
+ logger.info('Deadline reached for rcv_buffer src 0x%02X dst 0x%02X', buf['src_address'], buf['dest_address'] )
373
+ if buf['dest_address'] != ParameterGroupNumber.Address.GLOBAL:
374
+ self.__send_tp_abort(buf['dest_address'], buf['src_address'], buf['session'], self.ConnectionAbortReason.TIMEOUT, buf['pgn'])
375
+ del self._rcv_buffer[bufid]
376
+ self.__put_rts_cts_session(buf['session'])
377
+ else:
378
+ del self._rcv_buffer[bufid]
379
+ self.__put_bam_session(buf['session'])
380
+ # TODO: should we notify our CAs about the cancelled transfer?
381
+
382
+ # check multi-pg send buffers for timeout
383
+ for bufid in list(self._multi_pg_snd_buffer):
384
+ buf = self._multi_pg_snd_buffer[bufid]
385
+ if buf['deadline'] > now:
386
+ if next_wakeup > buf['deadline']:
387
+ next_wakeup = buf['deadline']
388
+ else:
389
+ # deadline reached
390
+ frame_format, session_num, src_address, dst_address = self._buffer_unhash_mpg(bufid)
391
+ self.__send_multi_pg(frame_format, buf['cpg'], src_address, dst_address)
392
+ del self._multi_pg_snd_buffer[bufid]
393
+
394
+ # check send buffers
395
+ for bufid in list(self._snd_buffer):
396
+ buf = self._snd_buffer[bufid]
397
+ if buf['deadline'] != 0:
398
+ if buf['deadline'] > now:
399
+ if next_wakeup > buf['deadline']:
400
+ next_wakeup = buf['deadline']
401
+ else:
402
+ # deadline reached
403
+ if buf['state'] == self.SendBufferState.WAITING_CTS:
404
+ logger.info('Deadline WAITING_CTS reached for snd_buffer src 0x%02X dst 0x%02X', buf['src_address'], buf['dest_address'] )
405
+ self.__send_tp_abort(buf['src_address'], buf['dest_address'], buf['session'], self.ConnectionAbortReason.TIMEOUT, buf['pgn'])
406
+ del self._snd_buffer[bufid]
407
+ self.__put_rts_cts_session(buf['session'])
408
+ # TODO: should we notify our CAs about the cancelled transfer?
409
+
410
+ elif buf['state'] == self.SendBufferState.SENDING_RTS_CTS:
411
+ while buf['next_packet_to_send'] < buf['num_segments']:
412
+ package = buf['next_packet_to_send']
413
+ self.__send_tp_dt(buf['src_address'], buf['dest_address'], buf['session'], package+1, buf['data'][package])
414
+
415
+ buf['next_packet_to_send'] += 1
416
+ # send end of message status
417
+ if (package+1) == buf['num_segments']:
418
+ self.__send_tp_eom_status(buf['src_address'], buf['dest_address'], buf['session'], buf['message_size'], buf['num_segments'], buf['pgn'])
419
+ buf['deadline'] = time.monotonic() + self.Timeout.T5
420
+ buf['state'] = self.SendBufferState.WAITING_EOM_ACK
421
+ break
422
+ elif package == buf['next_wait_on_cts']:
423
+ # wait on next cts
424
+ buf['state'] = self.SendBufferState.WAITING_CTS
425
+ buf['deadline'] = time.monotonic() + self.Timeout.T3
426
+ break
427
+ elif self._minimum_tp_rts_cts_dt_interval != None:
428
+ buf['deadline'] = time.monotonic() + self._minimum_tp_rts_cts_dt_interval
429
+ break
430
+
431
+ # recalc next wakeup
432
+ if next_wakeup > buf['deadline']:
433
+ next_wakeup = buf['deadline']
434
+
435
+ elif buf['state'] == self.SendBufferState.WAITING_EOM_ACK:
436
+ # TODO: should we inform the application about the eom ack timeout?
437
+ del self._snd_buffer[bufid]
438
+ self.__put_rts_cts_session(buf['session'])
439
+
440
+ elif buf['state'] == self.SendBufferState.EOM_ACK_RECEIVED:
441
+ # TODO: should we inform the application about the successful transmission?
442
+ del self._snd_buffer[bufid]
443
+ self.__put_rts_cts_session(buf['session'])
444
+
445
+ elif buf['state'] == self.SendBufferState.SENDING_BAM:
446
+ # send next broadcast message...
447
+ package = buf['next_packet_to_send']
448
+ self.__send_tp_dt(buf['src_address'], buf['dest_address'], buf['session'], package+1, buf['data'][package])
449
+ buf['next_packet_to_send'] += 1
450
+
451
+ if buf['next_packet_to_send'] < buf['num_segments']:
452
+ buf['deadline'] = time.monotonic() + self._minimum_tp_bam_dt_interval
453
+ # recalc next wakeup
454
+ if next_wakeup > buf['deadline']:
455
+ next_wakeup = buf['deadline']
456
+ else:
457
+ buf['state'] = self.SendBufferState.SENDING_EOM_STATUS
458
+ # recalc next wakeup
459
+ buf['deadline'] = time.monotonic() + self._minimum_tp_bam_dt_interval
460
+ if next_wakeup > buf['deadline']:
461
+ next_wakeup = buf['deadline']
462
+
463
+ elif buf['state'] == self.SendBufferState.SENDING_EOM_STATUS:
464
+ # done
465
+ self.__send_tp_eom_status(buf['src_address'], buf['dest_address'],
466
+ buf['session'],
467
+ buf['message_size'], buf['num_segments'], buf['pgn'])
468
+ del self._snd_buffer[bufid]
469
+ self.__put_bam_session(buf['session'])
470
+ elif buf['state'] == self.SendBufferState.TRANSMISSION_FINISHED:
471
+ del self._snd_buffer[bufid]
472
+ else:
473
+ logger.critical('unknown SendBufferState %d', buf['state'])
474
+ del self._snd_buffer[bufid]
475
+
476
+ return next_wakeup
477
+
478
+
479
+ def _process_tp_cm(self, mid, dest_address, data, timestamp):
480
+ """Processes a Transport Protocol Connection Management (TP.CM) message
481
+
482
+ :param j1939.MessageId mid:
483
+ A MessageId object holding the information extracted from the can_id.
484
+ :param int dest_address:
485
+ The destination address of the message
486
+ :param bytearray data:
487
+ The data contained in the can-message.
488
+ :param float timestamp:
489
+ The timestamp the message was received (mostly) in fractions of Epoch-Seconds.
490
+ """
491
+
492
+ # check minimum tp-cm length
493
+ if len(data) < 12:
494
+ logger.info('tp-cm with incorrect dlc received, id', mid )
495
+ return
496
+
497
+ src_address = mid.source_address
498
+ control_byte = data[0] & 0xF
499
+ session_num = (data[0] >> 4) & 0xF
500
+ message_size = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8) | ((data[3] & 0xFF) << 16)
501
+ segment_num = (data[4] & 0xFF) | ((data[5] & 0xFF) << 8) | ((data[6] & 0xFF) << 16)
502
+ pgn = (data[9] & 0xFF) | ((data[10] & 0xFF) << 8) | ((data[11] & 0xFF) << 16)
503
+
504
+ with self._buffer_lock:
505
+ if control_byte == self.TpControlType.RTS:
506
+ buffer_hash = self._buffer_hash(session_num, src_address, dest_address)
507
+ num_segments = data[7] # Maximum number of segments that can be sent in response to one CTS.
508
+
509
+ if buffer_hash in self._rcv_buffer:
510
+ # according SAE J1939-22 we have to send an ABORT if an active
511
+ # transmission is already established
512
+ self.__send_tp_abort(dest_address, src_address, session_num, self.ConnectionAbortReason.BUSY, pgn)
513
+ self.__put_rts_cts_session(session_num)
514
+ return
515
+
516
+ # limit max number segments
517
+ num_segments = min(num_segments, segment_num)
518
+
519
+ # open new buffer for this connection
520
+ self._rcv_buffer[buffer_hash] = {
521
+ 'pgn': pgn,
522
+ 'session': session_num,
523
+ 'message_size': message_size, # total message size, number of bytes
524
+ 'num_segments': segment_num, # total number of segments
525
+ 'next_packet': 1,
526
+ 'next_cts_border': min(self._max_cmdt_packets, num_segments),
527
+ 'num_segments_max_rec': min(self._max_cmdt_packets, num_segments),
528
+ 'data': [],
529
+ 'deadline': time.monotonic() + self.Timeout.T2,
530
+ 'src_address' : src_address,
531
+ 'dest_address' : dest_address,
532
+ }
533
+ self.__send_tp_cts(dest_address, src_address, session_num, self._rcv_buffer[buffer_hash]['num_segments_max_rec'], 1, pgn)
534
+ self.__job_thread_wakeup()
535
+
536
+ elif control_byte == self.TpControlType.CTS:
537
+ buffer_hash = self._buffer_hash(session_num, dest_address, src_address)
538
+ num_segments = data[7] # Maximum number of segments that can be sent
539
+ if buffer_hash not in self._snd_buffer:
540
+ self.__send_tp_abort(dest_address, src_address, session_num, self.ConnectionAbortReason.RESOURCES, pgn)
541
+ self.__put_rts_cts_session(session_num)
542
+ return
543
+ if num_segments == 0:
544
+ # SAE J1939/22
545
+ # receiver requests a pause
546
+ self._snd_buffer[buffer_hash]['deadline'] = time.monotonic() + self.Timeout.Th
547
+ self.__job_thread_wakeup()
548
+ return
549
+
550
+ num_segments_all = self._snd_buffer[buffer_hash]['num_segments']
551
+ self._snd_buffer[buffer_hash]['next_packet_to_send'] = segment_num - 1
552
+ segments_to_be_sent = num_segments_all - self._snd_buffer[buffer_hash]['next_packet_to_send']
553
+ if num_segments > num_segments_all:
554
+ logger.debug("CTS: Allowed more packets %d than complete transmission %d", num_segments, num_segments_all)
555
+ num_segments = num_segments_all
556
+ if num_segments > self._max_cmdt_packets:
557
+ logger.debug("CTS: Allowed more packets %d than transmitters max-cmdt-number %d", num_segments, self._max_cmdt_packets)
558
+ num_segments = self._max_cmdt_packets
559
+ if num_segments > segments_to_be_sent:
560
+ logger.debug("CTS: Allowed more packets %d than needed to complete transmission %d", num_segments, segments_to_be_sent)
561
+ num_segments = segments_to_be_sent
562
+
563
+ self._snd_buffer[buffer_hash]['next_wait_on_cts'] = self._snd_buffer[buffer_hash]['next_packet_to_send'] + num_segments - 1
564
+
565
+ self._snd_buffer[buffer_hash]['state'] = self.SendBufferState.SENDING_RTS_CTS
566
+ self._snd_buffer[buffer_hash]['deadline'] = time.monotonic() # wake up immediately
567
+ self.__job_thread_wakeup()
568
+
569
+ elif control_byte == self.TpControlType.EOM_STATUS:
570
+ buffer_hash = self._buffer_hash(session_num, src_address, dest_address)
571
+ if buffer_hash not in self._rcv_buffer:
572
+ self.__put_rts_cts_session(session_num)
573
+ return
574
+ pgn = self._rcv_buffer[buffer_hash]['pgn']
575
+ if (self._rcv_buffer[buffer_hash]['message_size'] == message_size) and (self._rcv_buffer[buffer_hash]['num_segments'] == segment_num):
576
+ self.__notify_subscribers(mid.priority, pgn, src_address, dest_address, timestamp, self._rcv_buffer[buffer_hash]['data'])
577
+ if dest_address != ParameterGroupNumber.Address.GLOBAL:
578
+ self.__send_tp_eom_ack(dest_address, src_address, session_num, message_size, segment_num, pgn)
579
+ else:
580
+ self.__send_tp_abort(dest_address, src_address, session_num, self.ConnectionAbortReason.RESOURCES, pgn)
581
+ del self._rcv_buffer[buffer_hash]
582
+ self.__put_rts_cts_session(session_num)
583
+
584
+ elif control_byte == self.TpControlType.EOM_ACK:
585
+ buffer_hash = self._buffer_hash(session_num, dest_address, src_address)
586
+ if buffer_hash not in self._snd_buffer:
587
+ self.__send_tp_abort(dest_address, src_address, session_num, self.ConnectionAbortReason.RESOURCES, pgn)
588
+ self.__put_rts_cts_session(session_num)
589
+ return
590
+ # TODO: should we inform the application about the successful transmission?
591
+ # Notify subscribers here to be used for the memory access server to know when to send operation complete
592
+ self.__notify_subscribers(mid.priority, pgn, mid.source_address, dest_address, timestamp, data)
593
+ self._snd_buffer[buffer_hash]['state'] = self.SendBufferState.EOM_ACK_RECEIVED
594
+ self._snd_buffer[buffer_hash]['deadline'] = time.monotonic() # wake up immediately
595
+ self.__job_thread_wakeup()
596
+
597
+ # BAM FD.TP.CM received
598
+ elif control_byte == self.TpControlType.BAM:
599
+ buffer_hash = self._buffer_hash(session_num, src_address, dest_address)
600
+ if buffer_hash in self._rcv_buffer:
601
+ # buffer already in use
602
+ logger.info('bam receive buffer already in use 0x%x', buffer_hash )
603
+ del self._rcv_buffer[buffer_hash]
604
+ self.__put_bam_session(session_num)
605
+ return
606
+
607
+ # init new buffer for this connection
608
+ self._rcv_buffer[buffer_hash] = {
609
+ 'pgn': pgn,
610
+ 'session': session_num,
611
+ 'message_size': message_size, # Total message size, number of bytes
612
+ 'num_segments': segment_num, # Total number of segments
613
+ 'next_packet': 1,
614
+ 'data': [],
615
+ 'deadline': time.monotonic() + self.Timeout.T1,
616
+ 'src_address' : src_address,
617
+ 'dest_address' : dest_address,
618
+ }
619
+ self.__job_thread_wakeup()
620
+
621
+ elif control_byte == self.TpControlType.ABORT:
622
+ # if abort received before transmission established -> cancel transmission
623
+ buffer_hash = self._buffer_hash(session_num, dest_address, src_address)
624
+ if buffer_hash in self._snd_buffer and self._snd_buffer[buffer_hash]['state'] == self.SendBufferState.WAITING_CTS:
625
+ # cancel transmission
626
+ self._snd_buffer[buffer_hash]['state'] = self.SendBufferState.TRANSMISSION_FINISHED
627
+ self._snd_buffer[buffer_hash]['deadline'] = time.monotonic()
628
+ # TODO: any more abort responses?
629
+ else:
630
+ raise RuntimeError('Received TP.CM with unknown control_byte %d', control_byte)
631
+
632
+ def _process_tp_dt(self, mid, dest_address, data, timestamp):
633
+
634
+ # check minimum tp-dt length
635
+ if len(data) <= 4:
636
+ logger.info('tp-dt with incorrect dlc received, id', mid )
637
+ return
638
+
639
+ src_address = mid.source_address
640
+ dtfi = data[0] & 0xF # Data Transfer Format Indicator
641
+ session_num = (data[0] >> 4) & 0xF
642
+ segment_num = (data[1] & 0xFF) | ((data[2] & 0xFF) << 8) | ((data[3] & 0xFF) << 16)
643
+
644
+ if segment_num == 0:
645
+ logger.critical('segment number of 0 is not valid.')
646
+ return
647
+
648
+ buffer_hash = self._buffer_hash(session_num, src_address, dest_address)
649
+
650
+ with self._buffer_lock:
651
+ if buffer_hash not in self._rcv_buffer:
652
+ logger.critical('buffer error process dt 0x%x', buffer_hash)
653
+ return
654
+
655
+ if self._rcv_buffer[buffer_hash]['next_packet'] != segment_num:
656
+ logger.critical('packet error. required: '+ str(self._rcv_buffer[buffer_hash]['next_packet']) + ' received: ' + str(segment_num) )
657
+ return
658
+
659
+ # get data
660
+ self._rcv_buffer[buffer_hash]['data'].extend(data[4:])
661
+
662
+ self._rcv_buffer[buffer_hash]['next_packet'] = segment_num + 1
663
+
664
+ # message is complete with sending an acknowledge
665
+ if len(self._rcv_buffer[buffer_hash]['data']) >= self._rcv_buffer[buffer_hash]['message_size']:
666
+ logger.info('finished RCV of PGN {} with size {}'.format(self._rcv_buffer[buffer_hash]['pgn'], self._rcv_buffer[buffer_hash]['message_size']))
667
+ # shorten data to message_size
668
+ self._rcv_buffer[buffer_hash]['data'] = self._rcv_buffer[buffer_hash]['data'][:self._rcv_buffer[buffer_hash]['message_size']]
669
+ # finished reassembly
670
+ if dest_address != ParameterGroupNumber.Address.GLOBAL:
671
+ # set deadline for waiting on eom status
672
+ self._rcv_buffer[buffer_hash]['deadline'] = time.monotonic() + self.Timeout.T1
673
+ self.__job_thread_wakeup()
674
+ return
675
+
676
+ # send clear to send
677
+ if (dest_address != ParameterGroupNumber.Address.GLOBAL) and (segment_num >= self._rcv_buffer[buffer_hash]['next_cts_border']):
678
+ # send cts
679
+ number_of_packets_that_can_be_sent = min( self._rcv_buffer[buffer_hash]['num_segments_max_rec'], self._rcv_buffer[buffer_hash]['num_segments'] - self._rcv_buffer[buffer_hash]['next_cts_border'] )
680
+ next_packet_to_be_sent = self._rcv_buffer[buffer_hash]['next_cts_border'] + 1
681
+ self.__send_tp_cts(dest_address, src_address, session_num, number_of_packets_that_can_be_sent, next_packet_to_be_sent, self._rcv_buffer[buffer_hash]['pgn'])
682
+
683
+ # calculate next packet number at which a CTS is to be sent
684
+ self._rcv_buffer[buffer_hash]['next_cts_border'] = min(self._rcv_buffer[buffer_hash]['next_cts_border'] + self._rcv_buffer[buffer_hash]['num_segments_max_rec'],
685
+ self._rcv_buffer[buffer_hash]['num_segments'])
686
+
687
+ self._rcv_buffer[buffer_hash]['deadline'] = time.monotonic() + self.Timeout.T2
688
+ self.__job_thread_wakeup()
689
+ return
690
+
691
+ self._rcv_buffer[buffer_hash]['deadline'] = time.monotonic() + self.Timeout.T1
692
+
693
+ def _process_multi_pg(self, mid : MessageId, dest_address, data, timestamp):
694
+ # currently "SAE J1939 with no assurance data" trailer format supported only
695
+ src_address = mid.source_address
696
+
697
+ while True:
698
+ if len(data) <= 4:
699
+ break
700
+ tos = (data[0] >> 5) & 0x7
701
+ # padding service
702
+ if tos == 0:
703
+ break
704
+
705
+ trailer_format = (data[0] >> 2) & 0x7
706
+ cpgn = ((data[0] & 0x3) << 16) | (data[1] << 8) | data[2]
707
+ payload_length = (data[3] & 0xFF)
708
+ if (tos == 2) and (trailer_format == 0):
709
+ # SAE J1939 with no assurance data
710
+ self.__notify_subscribers(mid.priority, cpgn, src_address, dest_address, timestamp, data[4:(4+payload_length)].copy())
711
+ else:
712
+ # TODO
713
+ print('other tos/tf formats currently not supported')
714
+
715
+ # trim data
716
+ data = data[(4+payload_length):]
717
+
718
+ def __send_tp_abort(self, src_address, dest_address, session_num, reason, pgn_value):
719
+ self.__send_tp_cm(src_address, dest_address, self.TpControlType.ABORT, session_num, 0xFFFFFF, 0xFFFFFF, 0xFFFFFF, reason, pgn_value)
720
+
721
+ def __send_tp_rts(self, priority, src_address, dest_address, session_num, pgn_value, message_size, num_segments, max_cmdt_packets, adt=Adt.NO_ADT):
722
+ self.__send_tp_cm(src_address, dest_address, self.TpControlType.RTS, session_num, message_size, num_segments, max_cmdt_packets, adt, pgn_value, priority)
723
+
724
+ def __send_tp_cts(self, src_address, dest_address, session_num, num_segments_that_can_be_sent, next_packet, pgn_value):
725
+ request_code = 0
726
+ self.__send_tp_cm(src_address, dest_address, self.TpControlType.CTS, session_num, 0xFFFFFF, next_packet, num_segments_that_can_be_sent, request_code, pgn_value)
727
+
728
+ def __send_tp_eom_status(self, src_address, dest_address, session_num, message_size, num_segments, pgn_value, size_of_assurance_data=0, adt=Adt.NO_ADT):
729
+ self.__send_tp_cm(src_address, dest_address, self.TpControlType.EOM_STATUS, session_num, message_size, num_segments, size_of_assurance_data, adt, pgn_value)
730
+
731
+ def __send_tp_eom_ack(self, src_address, dest_address, session_num, message_size, num_segments, pgn_value):
732
+ self.__send_tp_cm(src_address, dest_address, self.TpControlType.EOM_ACK, session_num, message_size, num_segments, 0xFF, 0xFF, pgn_value)
733
+
734
+ def __send_tp_bam(self, priority, src_address, session_num, pgn_value, message_size, num_segments):
735
+ self.__send_tp_cm(src_address, ParameterGroupNumber.Address.GLOBAL, self.TpControlType.BAM, session_num, message_size, num_segments, 0xFF , 0, pgn_value, priority)
736
+
737
+ def __send_tp_cm(self, src_address, dest_address,
738
+ TpControlType : TpControlType, session_num, message_size,
739
+ num_segments, # total number of segments or next segment number to be sent
740
+ byte_7, # maximum number of segments or num of segments that can be sent or assurance data Size
741
+ byte_8, # assurance data type or request code or teason code:
742
+ pgn,
743
+ priority=7):
744
+
745
+ pgn_tp_cm = ParameterGroupNumber(0, (ParameterGroupNumber.PGN.FD_TP_CM>>8) & 0xFF, dest_address)
746
+ mid = MessageId(priority=priority, parameter_group_number=pgn_tp_cm.value, source_address=src_address)
747
+
748
+ data = [0] * 12
749
+ data[0] = ( (TpControlType & 0xF) | ((session_num & 0xF) << 4))
750
+ data[1] = ( message_size & 0xFF )
751
+ data[2] = ( (message_size >> 8) & 0xFF )
752
+ data[3] = ( (message_size >> 16) & 0xFF )
753
+ data[4] = ( num_segments & 0xFF )
754
+ data[5] = ( (num_segments >> 8) & 0xFF )
755
+ data[6] = ( (num_segments >> 16) & 0xFF )
756
+ data[7] = ( byte_7 & 0xFF )
757
+ data[8] = ( byte_8 & 0xFF )
758
+ data[9] = ( pgn & 0xFF )
759
+ data[10] = ( (pgn >> 8) & 0xFF )
760
+ data[11] = ( (pgn >> 16) & 0xFF )
761
+ # 13 up to 64 Assurance Data of full message calculated using AD Type. Total length = Size in byte 8.
762
+ self.__send_message(mid.can_id, True, data, fd_format=True)
763
+
764
+ def __send_tp_dt(self, src_address, dest_address, session_num, segment_num, data, Dtfi=0):
765
+ pgn = ParameterGroupNumber(0, (ParameterGroupNumber.PGN.FD_TP_DT>>8) & 0xFF, dest_address)
766
+ mid = MessageId(priority=7, parameter_group_number=pgn.value, source_address=src_address)
767
+
768
+ data.insert(0, (Dtfi & 0xF) | ((session_num & 0xF) << 4))
769
+ data.insert(1, segment_num & 0xFF)
770
+ data.insert(2, (segment_num >> 8) & 0xFF)
771
+ data.insert(3, (segment_num >> 16) & 0xFF)
772
+
773
+ next_valid_fd_length = 0
774
+ if len(data)>=(self.DataLength.TP+4):
775
+ data = data[:(self.DataLength.TP+4)]
776
+ else:
777
+ # padding
778
+ next_valid_fd_length = self._LUT_FD_DLC[len(data)]
779
+ if next_valid_fd_length < 0: next_valid_fd_length = 0
780
+
781
+ while len(data)<next_valid_fd_length:
782
+ data.append(255)
783
+
784
+ self.__send_message(mid.can_id, True, data, fd_format=True)
785
+
786
+
787
+ def notify(self, can_id, data, timestamp):
788
+ """Feed incoming CAN message into this ecu.
789
+
790
+ If a custom interface is used, this function must be called for each
791
+ 29-bit standard message read from the CAN bus.
792
+
793
+ :param int can_id:
794
+ CAN-ID of the message (always 29-bit)
795
+ :param bytearray data:
796
+ Data part of the message (0 - 8 bytes)
797
+ :param float timestamp:
798
+ The timestamp field in a CAN message is a floating point number
799
+ representing when the message was received since the epoch in
800
+ seconds.
801
+ Where possible this will be timestamped in hardware.
802
+ """
803
+ mid = MessageId(can_id=can_id)
804
+ pgn = ParameterGroupNumber()
805
+ pgn.from_message_id(mid)
806
+
807
+ # peer to peer
808
+ # pdu_specific is destination Address
809
+ pgn_value = pgn.value & 0x1FF00
810
+ dest_address = pgn.pdu_specific # may be Address.GLOBAL
811
+
812
+ # iterate all CAs to check if we have to handle this destination address
813
+ if dest_address != ParameterGroupNumber.Address.GLOBAL:
814
+ if not self.__ecu_is_message_acceptable(dest_address): # simple peer-to-peer reception without adding a controller-application
815
+ reject = True
816
+ for ca in self._cas:
817
+ if ca.message_acceptable(dest_address):
818
+ reject = False
819
+ break
820
+ if reject == True:
821
+ return
822
+
823
+ if pgn_value == ParameterGroupNumber.PGN.FEFF_MULTI_PG:
824
+ self._process_multi_pg(mid, dest_address, data, timestamp)
825
+ elif pgn_value == ParameterGroupNumber.PGN.ADDRESSCLAIM:
826
+ for ca in self._cas:
827
+ ca._process_addressclaim(mid, data, timestamp)
828
+ elif pgn_value == ParameterGroupNumber.PGN.REQUEST:
829
+ for ca in self._cas:
830
+ if ca.message_acceptable(dest_address):
831
+ ca._process_request(mid, dest_address, data, timestamp)
832
+ elif pgn_value == ParameterGroupNumber.PGN.FD_TP_CM:
833
+ self._process_tp_cm(mid, dest_address, data, timestamp)
834
+ elif pgn_value == ParameterGroupNumber.PGN.FD_TP_DT:
835
+ self._process_tp_dt(mid, dest_address, data, timestamp)
836
+ elif pgn_value == ParameterGroupNumber.PGN.TP_CM:
837
+ logger.info('j1939-21 transport protocol cm not allowed in j1939-22 network')
838
+ elif pgn_value == ParameterGroupNumber.PGN.DATATRANSFER:
839
+ logger.info('j1939-21 transport protocol dt not allowed in j1939-22 network')
840
+ elif pgn.is_pdu2_format:
841
+ # direct broadcast
842
+ self.__notify_subscribers(mid.priority, pgn.value, mid.source_address, ParameterGroupNumber.Address.GLOBAL, timestamp, data)
843
+ else:
844
+ self.__notify_subscribers(mid.priority, pgn_value, mid.source_address, dest_address, timestamp, data)
845
+