@@ -206,3 +206,240 @@ def print_log_summary(self, device_id, log_file, df_phys):
206
206
"\n ---------------" ,
207
207
f"\n Device: { device_id } | Log file: { log_file .split (device_id )[- 1 ]} [Extracted { len (df_phys )} decoded frames]\n Period: { df_phys .index .min ()} - { df_phys .index .max ()} \n " ,
208
208
)
209
+
210
+
211
+ # -----------------------------------------------
212
+ class MultiFrameDecoder :
213
+ """BETA class for handling transport protocol data. For each response ID, identify
214
+ sequences of subsequent frames and combine the relevant parts of the data payloads
215
+ into a single payload with the response ID as the ID. The original raw dataframe is
216
+ then cleansed of the original response ID sequence frames. Instead, the new concatenated
217
+ frames are inserted. Further, the class supports DBC decoding of the resulting modified raw data
218
+
219
+ :param tp_type: the class supports UDS ("uds"), NMEA 2000 Fast Packets ("nmea") and J1939 ("j1939")
220
+ :param df_raw: dataframe of raw CAN data from the mdf_iter module
221
+
222
+ SINGLE_FRAME_MASK: mask used in matching single frames
223
+ FIRST_FRAME_MASK: mask used in matching first frames
224
+ CONSEQ_FRAME_MASK: mask used in matching consequtive frames
225
+ SINGLE_FRAME: frame type reflecting a single frame response
226
+ FIRST_FRAME: frame type reflecting the first frame in a multi frame response
227
+ CONSEQ_FRAME: frame type reflecting a consequtive frame in a multi frame response
228
+ ff_payload_start: the combined payload will start at this byte in the FIRST_FRAME
229
+ bam_pgn_hex: this is used in J1939 and marks the initial BAM message ID in HEX
230
+ res_id_list_hex: TP 'response CAN IDs' to process. For nmea/j1939, these are provided by default
231
+
232
+ """
233
+
234
+ def __init__ (self , tp_type = "" ):
235
+ frame_struct_uds = {
236
+ "SINGLE_FRAME_MASK" : 0xF0 ,
237
+ "FIRST_FRAME_MASK" : 0xF0 ,
238
+ "CONSEQ_FRAME_MASK" : 0xF0 ,
239
+ "SINGLE_FRAME" : 0x00 ,
240
+ "FIRST_FRAME" : 0x10 ,
241
+ "CONSEQ_FRAME" : 0x20 ,
242
+ "ff_payload_start" : 2 ,
243
+ "bam_pgn_hex" : "" ,
244
+ "res_id_list_hex" : ["0x7E0" , "0x7E9" , "0x7EA" , "0x7EB" , "0x7EC" , "0x7ED" , "0x7EE" , "0x7EF" , "0x7EA" , "0x7BB" ],
245
+ }
246
+
247
+ frame_struct_j1939 = {
248
+ "SINGLE_FRAME_MASK" : 0xFF ,
249
+ "FIRST_FRAME_MASK" : 0xFF ,
250
+ "CONSEQ_FRAME_MASK" : 0x00 ,
251
+ "SINGLE_FRAME" : 0xFF ,
252
+ "FIRST_FRAME" : 0x20 ,
253
+ "CONSEQ_FRAME" : 0x00 ,
254
+ "ff_payload_start" : 8 ,
255
+ "bam_pgn_hex" : "0xEC00" ,
256
+ "res_id_list_hex" : ["0xEB00" ],
257
+ }
258
+
259
+ frame_struct_nmea = {
260
+ "SINGLE_FRAME_MASK" : 0xFF ,
261
+ "FIRST_FRAME_MASK" : 0x0F ,
262
+ "CONSEQ_FRAME_MASK" : 0x00 ,
263
+ "SINGLE_FRAME" : 0xFF ,
264
+ "FIRST_FRAME" : 0x00 ,
265
+ "CONSEQ_FRAME" : 0x00 ,
266
+ "ff_payload_start" : 2 ,
267
+ "bam_pgn_hex" : "" ,
268
+ "res_id_list_hex" : [
269
+ "0xfed8" ,
270
+ "0x1f007" ,
271
+ "0x1f008" ,
272
+ "0x1f009" ,
273
+ "0x1f014" ,
274
+ "0x1f016" ,
275
+ "0x1f101" ,
276
+ "0x1f105" ,
277
+ "0x1f201" ,
278
+ "0x1f208" ,
279
+ "0x1f209" ,
280
+ "0x1f20a" ,
281
+ "0x1f20c" ,
282
+ "0x1f20f" ,
283
+ "0x1f210" ,
284
+ "0x1f212" ,
285
+ "0x1f513" ,
286
+ "0x1f805" ,
287
+ "0x1f80e" ,
288
+ "0x1f80f" ,
289
+ "0x1f810" ,
290
+ "0x1f811" ,
291
+ "0x1f814" ,
292
+ "0x1f815" ,
293
+ "0x1f904" ,
294
+ "0x1f905" ,
295
+ "0x1fa04" ,
296
+ "0x1fb02" ,
297
+ "0x1fb03" ,
298
+ "0x1fb04" ,
299
+ "0x1fb05" ,
300
+ "0x1fb11" ,
301
+ "0x1fb12" ,
302
+ "0x1fd10" ,
303
+ "0x1fe07" ,
304
+ "0x1fe12" ,
305
+ "0x1ff14" ,
306
+ "0x1ff15" ,
307
+ ],
308
+ }
309
+
310
+ if tp_type == "uds" :
311
+ self .frame_struct = frame_struct_uds
312
+ elif tp_type == "j1939" :
313
+ self .frame_struct = frame_struct_j1939
314
+ elif tp_type == "nmea" :
315
+ self .frame_struct = frame_struct_nmea
316
+ else :
317
+ self .frame_struct = {}
318
+
319
+ self .tp_type = tp_type
320
+
321
+ return
322
+
323
+ def calculate_pgn (self , frame_id ):
324
+ pgn = (frame_id & 0x03FFFF00 ) >> 8
325
+
326
+ pgn_f = (pgn & 0xFF00 ) >> 8
327
+ pgn_s = pgn & 0x00FF
328
+
329
+ if pgn_f < 240 :
330
+ pgn &= 0xFFFFFF00
331
+
332
+ return pgn
333
+
334
+ def construct_new_tp_frame (self , base_frame , payload_concatenated , can_id ):
335
+ new_frame = base_frame
336
+ new_frame .at ["DataBytes" ] = payload_concatenated
337
+ new_frame .at ["DLC" ] = 0
338
+ new_frame .at ["DataLength" ] = len (payload_concatenated )
339
+
340
+ if can_id :
341
+ new_frame .at ["ID" ] = can_id
342
+
343
+ return new_frame
344
+
345
+ def combine_tp_frames (self , df_raw ):
346
+ import pandas as pd
347
+ import sys
348
+
349
+ bam_pgn_hex = self .frame_struct ["bam_pgn_hex" ]
350
+ res_id_list = [int (res_id , 16 ) for res_id in self .frame_struct ["res_id_list_hex" ]]
351
+
352
+ df_raw_combined = pd .DataFrame ()
353
+
354
+ # use PGN matching for J1939 and NMEA
355
+ if self .tp_type == "nmea" or self .tp_type == "j1939" :
356
+ df_raw_excl_tp = df_raw [~ df_raw ["ID" ].apply (self .calculate_pgn ).isin (res_id_list )]
357
+ else :
358
+ df_raw_excl_tp = df_raw [~ df_raw ["ID" ].isin (res_id_list )]
359
+
360
+ df_raw_combined = df_raw_excl_tp
361
+
362
+ for channel , df_raw_channel in df_raw .groupby ("BusChannel" ):
363
+ for res_id in res_id_list :
364
+ # filter raw data for response ID and extract a 'base frame'
365
+ if bam_pgn_hex == "" :
366
+ bam_pgn = 0
367
+ else :
368
+ bam_pgn = int (bam_pgn_hex , 16 )
369
+
370
+ if self .tp_type == "nmea" or self .tp_type == "j1939" :
371
+ df_raw_filter = df_raw_channel [df_raw_channel ["ID" ].apply (self .calculate_pgn ).isin ([res_id , bam_pgn ])]
372
+ else :
373
+ df_raw_filter = df_raw_channel [df_raw_channel ["ID" ].isin ([res_id ])]
374
+
375
+ if df_raw_filter .empty :
376
+ continue
377
+
378
+ base_frame = df_raw_filter .iloc [0 ]
379
+
380
+ frame_list = []
381
+ frame_timestamp_list = []
382
+ payload_concatenated = []
383
+ ff_length = 0xFFF
384
+ can_id = None
385
+ conseq_frame_prev = None
386
+
387
+ # iterate through rows in filtered dataframe
388
+ for index , row in df_raw_filter .iterrows ():
389
+ payload = row ["DataBytes" ]
390
+ first_byte = payload [0 ]
391
+ row_id = row ["ID" ]
392
+ row_pgn = self .calculate_pgn (row_id )
393
+
394
+ # check if first frame (either for UDS/NMEA or J1939 case)
395
+ first_frame_test = (
396
+ (first_byte & self .frame_struct ["FIRST_FRAME_MASK" ] == self .frame_struct ["FIRST_FRAME" ])
397
+ & (bam_pgn_hex == "" )
398
+ ) or (self .tp_type == "j1939" and bam_pgn == row_pgn )
399
+
400
+ # if single frame, save frame directly (excl. 1st byte)
401
+ if first_byte & self .frame_struct ["SINGLE_FRAME_MASK" ] == self .frame_struct ["SINGLE_FRAME" ]:
402
+ new_frame = self .construct_new_tp_frame (base_frame , payload , row_id )
403
+ frame_list .append (new_frame .values .tolist ())
404
+ frame_timestamp_list .append (index )
405
+
406
+ # if first frame, save info from prior multi frame response sequence,
407
+ # then initialize a new sequence incl. the first frame payload
408
+ elif first_frame_test :
409
+ # create a new frame using information from previous iterations
410
+ if len (payload_concatenated ) >= ff_length :
411
+ new_frame = self .construct_new_tp_frame (base_frame , payload_concatenated , can_id )
412
+
413
+ frame_list .append (new_frame .values .tolist ())
414
+ frame_timestamp_list .append (frame_timestamp )
415
+
416
+ # reset and start on next frame
417
+ payload_concatenated = []
418
+ conseq_frame_prev = None
419
+ frame_timestamp = index
420
+
421
+ # for J1939, extract PGN and convert to 29 bit CAN ID for use in baseframe
422
+ if self .tp_type == "j1939" :
423
+ pgn_hex = "" .join ("{:02x}" .format (x ) for x in reversed (payload [5 :8 ]))
424
+ pgn = int (pgn_hex , 16 )
425
+ can_id = (6 << 26 ) | (pgn << 8 ) | 254
426
+
427
+ ff_length = (payload [0 ] & 0x0F ) << 8 | payload [1 ]
428
+
429
+ for byte in payload [self .frame_struct ["ff_payload_start" ] :]:
430
+ payload_concatenated .append (byte )
431
+
432
+ # if consequtive frame, extend payload with payload excl. 1st byte
433
+ elif first_byte & self .frame_struct ["CONSEQ_FRAME_MASK" ] == self .frame_struct ["CONSEQ_FRAME" ]:
434
+ if (conseq_frame_prev == None ) or ((first_byte - conseq_frame_prev ) == 1 ):
435
+ conseq_frame_prev = first_byte
436
+ for byte in payload [1 :]:
437
+ payload_concatenated .append (byte )
438
+
439
+ df_raw_tp = pd .DataFrame (frame_list , columns = base_frame .index , index = frame_timestamp_list )
440
+ df_raw_combined = df_raw_combined .append (df_raw_tp )
441
+
442
+ df_raw_combined .index .name = "TimeStamp"
443
+ df_raw_combined = df_raw_combined .sort_index ()
444
+
445
+ return df_raw_combined
0 commit comments