電子數(shù)據(jù)格式轉(zhuǎn)換:MDF/ASC/BLF/MAT/BMR統(tǒng)一處理實(shí)現(xiàn))
簡(jiǎn)介這款工具面向IT運(yùn)維、嵌入式開(kāi)發(fā)及數(shù)據(jù)分析人員用于處理BMR、MDF、MAT、ASC、BLF五種常見(jiàn)trace與日志格式可廣泛支撐嵌入式系統(tǒng)調(diào)試、數(shù)據(jù)庫(kù)日志歸檔、JVM內(nèi)存轉(zhuǎn)儲(chǔ)分析等場(chǎng)景解決多源數(shù)據(jù)格式不統(tǒng)一、難以集中分析的問(wèn)題。資源包共1688個(gè)文件壓縮后約128.23MB其中1124個(gè)頭文件、101個(gè)動(dòng)態(tài)鏈接庫(kù)文件和23個(gè)可執(zhí)行文件構(gòu)成核心程序另含日志文件、BMR樣例、PNG圖標(biāo)、XML與JSON配置等輔助內(nèi)容目錄結(jié)構(gòu)完整既能支撐工具運(yùn)行也方便使用者研究格式結(jié)構(gòu)或進(jìn)行二次開(kāi)發(fā)。已有1920人學(xué)習(xí)下載適合需要整合異構(gòu)日志、快速定位系統(tǒng)異常的中高級(jí)技術(shù)人員。借助該工具可將不同格式的trace數(shù)據(jù)批量轉(zhuǎn)換為統(tǒng)一格式實(shí)現(xiàn)集中檢索、關(guān)聯(lián)與歸檔例如可先利用內(nèi)置BMR或其他樣例驗(yàn)證解析邏輯再導(dǎo)入實(shí)際業(yè)務(wù)日志完成批量處理。隨包附帶的配置模板與依賴庫(kù)能明顯降低部署門(mén)檻適合在調(diào)試、測(cè)試和故障排查場(chǎng)景中直接使用。1. 一個(gè)真實(shí)場(chǎng)景五種格式拼接起來(lái)的故障追溯事情要從去年底接手的一個(gè)整車(chē)網(wǎng)絡(luò)排查說(shuō)起。當(dāng)時(shí)臺(tái)架測(cè)試組把采集到的數(shù)據(jù)分成了好幾份丟過(guò)來(lái)ECU刷寫(xiě)階段用的是數(shù)據(jù)采集器導(dǎo)出的BMR文件整車(chē)道路測(cè)試的數(shù)據(jù)是CANoe記錄成BLF和ASC另外有一部分標(biāo)定數(shù)據(jù)被同事用INCA轉(zhuǎn)成了MDF最后做離線分析時(shí)還有人直接把通道拖去了MATLAB存成了MAT。我要做的第一件事就是把這幾份數(shù)據(jù)拼到同一條時(shí)間軸上對(duì)比同一個(gè)CAN信號(hào)的跳變時(shí)刻。麻煩很快出現(xiàn)。每份文件的讀取工具都不一樣MDF得開(kāi)CANape或者用Python讀ASC拿記事本勉強(qiáng)能看但幾萬(wàn)行直接卡死BLF如果沒(méi)有Vector工具鏈基本打不開(kāi)MAT導(dǎo)入Python還要考慮版本兼容。最要命的是這些格式之間沒(méi)有一個(gè)現(xiàn)成的轉(zhuǎn)換通道于是我在想與其每次手動(dòng)到處導(dǎo)來(lái)導(dǎo)去不如直接做一個(gè)統(tǒng)一的trace轉(zhuǎn)換工具把這幾種格式全部收進(jìn)來(lái)再按需輸出成目標(biāo)格式。這個(gè)工具折騰了兩個(gè)星期后來(lái)成了組里公共的腳本庫(kù)這篇就是我整理出來(lái)的完整實(shí)現(xiàn)思路。我不打算把一個(gè)成品工具丟給你就完事而是把當(dāng)初為什么這么設(shè)計(jì)、哪些庫(kù)能解決什么問(wèn)題、踩過(guò)哪些特別隱蔽的坑都攤開(kāi)來(lái)講。你在自己的項(xiàng)目里哪怕只用到其中一部分也應(yīng)該能省下不少時(shí)間。2. 格式差異與轉(zhuǎn)換的第一性問(wèn)題先建中間數(shù)據(jù)模型再做格式映射在寫(xiě)任何一行轉(zhuǎn)換代碼之前我先把五種格式的來(lái)源、結(jié)構(gòu)、特性理了一遍。你如果直接用格式A解析類(lèi)往格式B寫(xiě)出類(lèi)里塞多半會(huì)在半路上被各種字段定義差異搞崩。正確的做法是先抽出一個(gè)統(tǒng)一的中間數(shù)據(jù)模型所有格式解析完都落到這個(gè)模型上再?gòu)倪@個(gè)模型生成任意目標(biāo)格式。下面這張表是我當(dāng)時(shí)整理的格式對(duì)比也是整個(gè)工具的基礎(chǔ)認(rèn)知格式常見(jiàn)來(lái)源本質(zhì)結(jié)構(gòu)讀取難點(diǎn)BMR部分?jǐn)?shù)據(jù)采集器/設(shè)備廠商導(dǎo)出二進(jìn)制流通常帶通道清單和定長(zhǎng)/變長(zhǎng)記錄各家定義不統(tǒng)一字段邊界不透明MDFINCA、CANape、ASAM標(biāo)準(zhǔn)測(cè)量文件分組(ID Block/HD Block/CG Block/CN Block)存儲(chǔ)支持通道樹(shù)版本多MDF3/MDF4通道模式復(fù)雜MATMATLAB 數(shù)據(jù)文件基于MAT-File Level 4/5/7.3格式內(nèi)部為數(shù)組結(jié)構(gòu)低版本hdf5/高版本h5各有差異編碼兼容ASCCANoe/CANalyzer導(dǎo)出的文本日志每行一條CAN幀或錯(cuò)誤幀時(shí)間戳在前文本行結(jié)構(gòu)隨工具版本變化轉(zhuǎn)義復(fù)雜BLFCANoe/CANalyzer二進(jìn)制日志文件頭壓縮塊對(duì)象頭幀數(shù)據(jù)需要按照對(duì)象header類(lèi)型逐條解析這個(gè)表做完核心設(shè)計(jì)就清楚了我需要一個(gè)Signal層用它來(lái)統(tǒng)一表示一個(gè)通道隨時(shí)間變化的采樣序列。不管BMR里來(lái)的是定長(zhǎng)浮點(diǎn)數(shù)組還是ASC里只有報(bào)文ID和原始字節(jié)最終都要轉(zhuǎn)換成(timestamps, samples)兩個(gè)numpy數(shù)組。from dataclasses import dataclass import numpy as np dataclass class Signal: name: str samples: np.ndarray timestamps: np.ndarray unit: str comment: str 為什么一定要走中間層因?yàn)槲宸N格式對(duì)時(shí)間和通道這兩個(gè)概念的表達(dá)能力完全不同。ASC和BLF本身是報(bào)文級(jí)格式同一個(gè)信號(hào)在一秒內(nèi)可能出現(xiàn)幾百次采樣點(diǎn)是不等間隔的MDF則可以直接存等間隔的連續(xù)采樣。如果硬把ASC的報(bào)文序列映射成MDF的等間隔采樣必然要先解決重采樣的問(wèn)題。中間層正好隔離了這層復(fù)雜度解析器負(fù)責(zé)把各自格式的重采樣/去重邏輯處理好輸出器只需要讀Signal不需要關(guān)心它到底來(lái)自哪種文件。3. 完整實(shí)現(xiàn)從MDF/ASC/BLF讀入到MAT/BMR寫(xiě)出3.1 依賴環(huán)境與版本選擇這一步就藏著坑我先說(shuō)我最后固定的環(huán)境這些版本組合實(shí)測(cè)下來(lái)兼容性最好python3.10 asammdf7.4.0 python-can4.3.1 numpy1.24 scipy1.10很多人會(huì)忽略版本組合的問(wèn)題但我在這上面吃了虧。asammdf如果版本太老MDF4.1的新特性讀不了版本太新又會(huì)依賴底層numpy的API變化。python-can 4.x把ASC和BLF讀取器都內(nèi)置了而且基于內(nèi)存映射的BLF讀取在4.3上性能有明顯提升所以這個(gè)版本組合基本是后期最穩(wěn)的一組。3.2 核心數(shù)據(jù)模型給五種格式一個(gè)共同出口我把Signal設(shè)計(jì)成最基礎(chǔ)的數(shù)據(jù)單元然后整個(gè)轉(zhuǎn)換工具只圍繞三種操作讀取器Reader返回一個(gè)list[Signal]過(guò)濾器Filter在這個(gè)列表上做通道篩選、重采樣、時(shí)間對(duì)齊寫(xiě)入器Writer接收這個(gè)列表并生成目標(biāo)格式。整個(gè)代碼結(jié)構(gòu)像流水線處理思路非常清晰。核心調(diào)度邏輯大概是def convert(input_path: str, output_path: str, output_format: str): signals read_by_ext(input_path) signals fill_time_gaps(signals) signals filter_channels(signals, ...) write_by_format(signals, output_path, output_format)read_by_ext和write_by_format就是兩張用文件擴(kuò)展名/輸出類(lèi)型做索引的工廠表新格式加入時(shí)只需要往表里加一個(gè)解析器和一個(gè)寫(xiě)入器其他邏輯不用動(dòng)。3.3 讀取側(cè)MDF解析值得重點(diǎn)說(shuō)asammdf是讀取MDF最省事的庫(kù)它對(duì)MDF3和MDF4都支持還內(nèi)置了一次性讀取全部通道、按通道名讀取、按分組讀取三種方式。我大文件場(chǎng)景下推薦按通道讀取而不是全部加載避免內(nèi)存被撐爆from asammdf import MDF def read_mdf(path: str, channel_pattern: str None): signals [] with MDF(path) as mdf: for name, info in mdf.channels_db.items(): if channel_pattern and channel_pattern not in name: continue sig mdf.get(name) signals.append(Signal( namename, samplessig.samples, timestampssig.timestamps, unitsig.unit, commentstr(sig.comment) )) return signals這里有個(gè)特別值得注意的細(xì)節(jié)mdf.get(name)返回的Signal里有samples和timestamps但是它們的 dtype 可能不一樣。有的MDF文件時(shí)間戳是浮點(diǎn)秒有的是納秒整型還有的是帶時(shí)區(qū)偏移的絕對(duì)時(shí)間。不統(tǒng)一單位就直接轉(zhuǎn)換數(shù)據(jù)錯(cuò)亂只是一瞬間的問(wèn)題。3.4 讀取側(cè)ASC和BLF的報(bào)文級(jí)處理思路ASC和BLF的記錄單位是CAN/CAN LIN幀不是通道信號(hào)。所以讀取它們的時(shí)候要先經(jīng)過(guò)一個(gè)信號(hào)提取步驟。簡(jiǎn)單場(chǎng)景下可以只保留原始報(bào)文用can.Message對(duì)象作為中間態(tài)如果要對(duì)里面某個(gè)信號(hào)做通道級(jí)處理就需要結(jié)合DBC文件解析信號(hào)。先看基礎(chǔ)版本只轉(zhuǎn)原始幀不動(dòng)DBC。用python-can的Reader非常直接import can def read_asc_or_blf(path: str): if path.endswith(.asc): source can.ASCReader(path) else: source can.BLFReader(path) frames [] with source as reader: for msg in reader: frames.append({ timestamp: msg.timestamp, arbitration_id: msg.arbitration_id, data: bytes(msg.data), is_extended_id: msg.is_extended_id, channel: msg.channel, }) return frames如果你的目標(biāo)是做通道級(jí)轉(zhuǎn)換比如把ASC里的車(chē)速信號(hào)轉(zhuǎn)成MDF通道那就需要引入DBC解碼。我的做法是把can這條鏈路讀取結(jié)果再送進(jìn)cantools逐幀解碼出信號(hào)值。這一層對(duì)性能影響很大所以我會(huì)在后面性能優(yōu)化部分專(zhuān)門(mén)講。3.5 寫(xiě)入側(cè)MAT導(dǎo)出和BMR序列化MAT格式對(duì)做數(shù)據(jù)分析的同事來(lái)說(shuō)是硬需求用scipy的savemat就能寫(xiě)。要注意MAT7.3版本基于HDF5大文件時(shí)推薦指定格式否則默認(rèn)的MAT5在文件超過(guò)2GB時(shí)很可能寫(xiě)失敗from scipy.io import savemat def write_mat(signals: list[Signal], path: str): data {} for sig in signals: data[sig.name] np.vstack([sig.timestamps, sig.samples]).T savemat(path, data, format5)BMR格式相對(duì)特殊因?yàn)樗鼪](méi)有一個(gè)完全統(tǒng)一的公開(kāi)協(xié)議標(biāo)準(zhǔn)。我這邊處理的BMR是某數(shù)據(jù)采集器導(dǎo)出的二進(jìn)制記錄格式結(jié)構(gòu)上就是一個(gè)頭后跟著若干條定長(zhǎng)記錄。我實(shí)現(xiàn)了一個(gè)通用寫(xiě)入器你也可以根據(jù)自己面對(duì)的廠商定義去調(diào)整import struct def write_bmr(signals: list[Signal], path: str): with open(path, wb) as f: f.write(bBMR1) f.write(struct.pack(I, len(signals))) for sig in signals: name_bytes sig.name.encode(utf-8) f.write(struct.pack(I, len(name_bytes))) f.write(name_bytes) f.write(sig.samples.astype(f8).tobytes()) f.write(sig.timestamps.astype(f8).tobytes())頭里我用了4字節(jié)整數(shù)存通道數(shù)量后面每個(gè)通道先寫(xiě)名字長(zhǎng)度、再寫(xiě)名字、再寫(xiě)數(shù)值數(shù)組和時(shí)間軸數(shù)組。這樣別人拿到的BMR文件就能通過(guò)我自己寫(xiě)的read_bmr重新讀回Signal保證閉環(huán)。4. 三個(gè)最難的坑時(shí)間戳精度、通道類(lèi)型、內(nèi)存峰值4.1 時(shí)間戳精度源頭不同必須統(tǒng)一基準(zhǔn)我最早轉(zhuǎn)換的時(shí)候就栽在這上面。ASC里時(shí)間戳默認(rèn)是CANoe啟動(dòng)后的相對(duì)時(shí)間單位是秒精確到微秒BLF里除了相對(duì)時(shí)間還可能帶硬件時(shí)間戳MDF里則可能是從ECU上電開(kāi)始的相對(duì)時(shí)間也可能是UTC絕對(duì)時(shí)間。我拿到的同一段測(cè)試ASC和MDF對(duì)同一個(gè)信號(hào)的時(shí)間軸差了8個(gè)小時(shí)因?yàn)镸DF那邊記錄的是帶時(shí)區(qū)偏移的絕對(duì)時(shí)間。解決辦法是在中間層統(tǒng)一一次時(shí)間基準(zhǔn)所有進(jìn)入Signal的時(shí)間戳統(tǒng)一轉(zhuǎn)成從文件頭第一條記錄開(kāi)始的相對(duì)時(shí)間單位用浮點(diǎn)秒同時(shí)在Signal里增加一個(gè)time_base字段記錄原始精度和是否絕對(duì)時(shí)間。轉(zhuǎn)換到目標(biāo)格式時(shí)再按需換算。千萬(wàn)不要在每個(gè)解析器里面自己處理時(shí)間戳格式那樣后面改需求會(huì)改到懷疑人生。4.2 通道類(lèi)型與字節(jié)對(duì)齊另一個(gè)隱蔽問(wèn)題是數(shù)據(jù)類(lèi)型。MDF里的通道可以是float32、float64、int16、uint64甚至帶位域偏移ASC/BLF里的CAN原始字節(jié)則可能是大端或者小端。如果你在做MDF轉(zhuǎn)BLF這種報(bào)文級(jí)轉(zhuǎn)換直接拿samples當(dāng)data寫(xiě)肯定出錯(cuò)。我處理方式是在Signal里加了一個(gè)raw_type字段記錄這個(gè)通道在原始文件里的類(lèi)型和字節(jié)序。寫(xiě)入BLF時(shí)用結(jié)構(gòu)體來(lái)還原import struct def pack_can_data(value, data_type: str, byte_order: str): fmt if byte_order little else if data_type float32: fmt f elif data_type uint16: fmt H else: raise ValueError(funsupported type {data_type}) return struct.pack(fmt, value)其實(shí)還有一個(gè)更常見(jiàn)的坑MATLAB里存的MAT文件經(jīng)常是行向量還是列向量的問(wèn)題。numpy里二維數(shù)組的shape如果不對(duì)MATLAB讀出來(lái)之后維度直接反了。建議統(tǒng)一按(N, 2)的時(shí)間戳數(shù)值列寫(xiě)入同時(shí)在注釋里寫(xiě)明白每一列的含義。4.3 大文件內(nèi)存峰值asammdf的流式讀取處理1GB以上的MDF時(shí)如果直接mdf.get(*)一次拉全量通道內(nèi)存直接飆升到好幾個(gè)GB。asammdf提供了按分組讀取的能力可以配合mdf.select按通道選擇后再處理with MDF(path) as mdf: selected mdf.select([EngineSpeed, VehicleSpeed]) for sig in selected: ...對(duì)于BLFpython-can的BLFReader本身就是按塊讀取的內(nèi)存壓力主要在后續(xù)信號(hào)解碼。我給自己的工具加了一個(gè)--channel-filter參數(shù)轉(zhuǎn)換前就把不需要的通道過(guò)濾掉實(shí)測(cè)對(duì)內(nèi)存峰值的影響非常明顯后面性能數(shù)據(jù)會(huì)體現(xiàn)。5. CLI工具設(shè)計(jì)與批量轉(zhuǎn)換的實(shí)踐取舍工具不能只在腳本里跑組里不同同事的使用習(xí)慣也不一樣我最后把它包裝成了一個(gè)命令行工具核心入口參數(shù)大概是python trace_convert.py \ --input raw_runs/ \ --output export/ \ --to mat \ --channel-filter VehicleSpeed|EngineSpeed \ --workers 4這個(gè)參數(shù)設(shè)計(jì)有幾個(gè)考慮。--input可以傳目錄也可以傳單文件傳目錄時(shí)自動(dòng)遞歸掃描所有支持的后綴省得一個(gè)個(gè)指定--to指定輸出格式工具會(huì)為每個(gè)輸入文件生成對(duì)應(yīng)的輸出文件名--channel-filter是正則表達(dá)式只保留匹配到的通道這對(duì)大文件特別有用--workers是并發(fā)進(jìn)程數(shù)因?yàn)椴煌募g的轉(zhuǎn)換沒(méi)有依賴關(guān)系用多進(jìn)程處理既簡(jiǎn)單又直觀。在批量轉(zhuǎn)換時(shí)我做了個(gè)小優(yōu)化先快速掃描一遍每個(gè)輸入文件的通道列表把通道過(guò)濾前置到解析階段。比如一個(gè)100通道的MDF文件如果你只需要其中的2個(gè)通道解析階段就只讀2個(gè)通道比全部讀出來(lái)再用numpy篩選快得多。實(shí)際效果同一個(gè)BLF文件全量解析過(guò)濾需要54秒前置過(guò)濾到2個(gè)通道后只要11秒。我用了進(jìn)程池來(lái)實(shí)現(xiàn)批量轉(zhuǎn)換而不是多線程。原因是asammdf和python-can在解析時(shí)都涉及大量的numpy計(jì)算在CPython的GIL限制下多線程基本占不到便宜多進(jìn)程才是正解。但注意多進(jìn)程時(shí)不能把Signal直接塞給子進(jìn)程再回傳因?yàn)樾蛄谢_(kāi)銷(xiāo)巨大。我的做法是每個(gè)子進(jìn)程獨(dú)立打開(kāi)輸入文件、獨(dú)立轉(zhuǎn)換、獨(dú)立寫(xiě)輸出文件父進(jìn)程只負(fù)責(zé)文件列表分派和錯(cuò)誤收集。6. 實(shí)測(cè)性能從跑不動(dòng)到跑進(jìn)3分鐘以內(nèi)光說(shuō)不練不行我拿自己手里一批測(cè)試數(shù)據(jù)做了對(duì)比。文件清單如下一份300MB的MDF4文件包含120個(gè)通道時(shí)長(zhǎng)1小時(shí)一份1.2GB的BLF文件約1800萬(wàn)幀CAN報(bào)文一份約200MB的ASC文件60萬(wàn)行文本一份用于校驗(yàn)輸出的MAT文件轉(zhuǎn)換場(chǎng)景初版耗時(shí)優(yōu)化后耗時(shí)主要優(yōu)化手段MDF - MAT96秒41秒按通道過(guò)濾、指定samples dtypeBLF - BMR132秒57秒前置通道過(guò)濾、少用逐幀Python循環(huán)ASC - MDF88秒36秒文本解析用向量化拆分減少列表appendBLF - ASC67秒22秒BLFReader直接遍歷不做多余對(duì)象拷貝這中間最值得說(shuō)的教訓(xùn)就是Python里能用numpy向量化解決的問(wèn)題就別用for循環(huán)。ASC那60萬(wàn)行文本一開(kāi)始我用split()逐行解析光解析就要50多秒后來(lái)?yè)Q成正則式按行批量處理再用numpy的fromstring把數(shù)字列一次性轉(zhuǎn)成數(shù)組快了三倍不止。BLF的讀取還有一個(gè)隱藏性能點(diǎn)BLF內(nèi)部是分塊存儲(chǔ)的python-can的BLFReader默認(rèn)會(huì)解壓每個(gè)塊。如果只是做通道過(guò)濾可以在讀取時(shí)就按msg.arbitration_id做一次初篩而不是全部進(jìn)內(nèi)存再篩。這個(gè)初篩邏輯要放在reader遍歷的循環(huán)里越早過(guò)濾越省內(nèi)存。整套優(yōu)化做完之后上面四條轉(zhuǎn)換路徑全部控制在1分鐘以內(nèi)。對(duì)日常需要反復(fù)對(duì)比多份數(shù)據(jù)的人來(lái)說(shuō)這個(gè)速度基本可以接受。7. 只有實(shí)際做過(guò)才知道的幾個(gè)小細(xì)節(jié)最后分享幾個(gè)做這個(gè)工具時(shí)積累下來(lái)的小經(jīng)驗(yàn)算不上什么大道理但能幫你少走彎路。第一個(gè)是關(guān)于DBC解碼的高成本。如果ASC/BLF里的信號(hào)需要結(jié)合DBC轉(zhuǎn)成物理值解碼那一步本身會(huì)成為新的性能瓶頸。后來(lái)我發(fā)現(xiàn)可以先只在時(shí)間軸上抽稀再做DBC解碼比如要畫(huà)曲線圖時(shí)每秒保留一個(gè)采樣點(diǎn)就夠了解碼速度能快幾十倍。這個(gè)思路在原始數(shù)據(jù)量很大、但下游分析只需要看趨勢(shì)的時(shí)候非常實(shí)用。第二個(gè)是輸出文件命名。批量轉(zhuǎn)換時(shí)如果直接拿輸入文件名換后綴同一個(gè)源文件在多次轉(zhuǎn)換中很容易被覆蓋。我加了一個(gè)時(shí)間戳后綴比如vehicle_runs_20241117_153000.mat文件多的時(shí)候找起來(lái)方便很多也避免了誤覆蓋。第三個(gè)是關(guān)于MAT文件的MATLAB兼容性。如果你轉(zhuǎn)換出來(lái)的MAT文件在MATLAB里打開(kāi)報(bào)錯(cuò)先檢查是不是默認(rèn)存成了MAT7.3因?yàn)楹芏嗬习姹綧ATLAB對(duì)HDF5格式的支持并不好。如果確定是給舊版本MATLAB用保存時(shí)填format5可能更穩(wěn)妥。這個(gè)工具現(xiàn)在還在持續(xù)往里加格式最近在考慮支持CSV和Excel導(dǎo)出方便非軟件背景的測(cè)試工程師直接打開(kāi)看。如果你也在被這些trace格式反復(fù)折磨希望這篇里的設(shè)計(jì)思路和代碼片段能幫你搭出第一版工具。轉(zhuǎn)換工具這種活兒做得越通用后面省的時(shí)間就越多。本文還有配套的精品資源點(diǎn)擊獲取