Skip to content

Commit 479353c

Browse files
author
Martin
committed
Updated utils.py to match api-examples repo. Updated main.py to include transport protocol class by default. Updated README.
1 parent f0d878b commit 479353c

File tree

3 files changed

+239
-23
lines changed

3 files changed

+239
-23
lines changed

README.md

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -80,29 +80,8 @@ If your log files contain data from two CAN channels, you may need to adjust the
8080
### Advanced processing (custom signals, transport protocol decoding, ...)
8181
If you need to perform more advanced data processing, you may find useful functions and examples in the api-examples library under `data-processing/`.
8282

83-
For example, if you need to decode TP data, you can include the `utils_tp.py` in this repo and modify your `main.py` as below:
83+
In particular, see the guide in that repository for including transport protocol handling for UDS, J1939 or NMEA 2000 fast packets.
8484

85-
```
86-
from utils_tp import MultiFrameDecoder
87-
...
88-
res_id_list_hex = ["0x7EC", "0x7BB"]
89-
tp_type = "uds"
90-
...
91-
tp = MultiFrameDecoder(df_raw, res_id_list_hex)
92-
df_raw = tp.combine_tp_frames_by_type(tp_type)
93-
df_phys = proc.extract_phys(df_raw, tp_type=tp_type)
94-
...
95-
```
96-
97-
For the case of NMEA 2000 multiframe fast packets, you can utilize the below details:
98-
99-
```
100-
from utils_tp import MultiFrameDecoder, nmea_fast_packet_pgns
101-
...
102-
res_id_list_hex = nmea_fast_packet_pgns
103-
tp_type = "nmea"
104-
105-
```
10685
---
10786

10887
### Add InfluxDB tags

dashboard-writer/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from utils import setup_fs, load_dbc_files, list_log_files, ProcessData
1+
from utils import setup_fs, load_dbc_files, list_log_files, ProcessData, MultiFrameDecoder
22
from utils_db import SetupInflux
33
import inputs as inp
44

dashboard-writer/utils.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,3 +206,240 @@ def print_log_summary(self, device_id, log_file, df_phys):
206206
"\n---------------",
207207
f"\nDevice: {device_id} | Log file: {log_file.split(device_id)[-1]} [Extracted {len(df_phys)} decoded frames]\nPeriod: {df_phys.index.min()} - {df_phys.index.max()}\n",
208208
)
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

Comments
 (0)