通过这个小软件可以很方便分析周期不稳定的报文,辅助快速解决丢包等问题。
一、功能描述:
1.可以打开busmaster软件的log格式报文,以及asc格式,还有周立功CAN记录仪的csv格式。
2.打开后自动分析每条报文的间隔,每个CANID+通道+方向视为一种报文,即通道1和通道2都有某条报文,分析时认为是两种报文。
3.表格支持点击表头排序,可以很方便看到偏差大的报文。
4.偏差计算方式为最大间隔除以平均间隔,并用颜色标注。
5.支持图表显示,有周期分布和次数分布两种。
6.支持表格内容保存。
7.支持查找报文功能
8.文件支持拖拽
9.支持标准帧和扩展帧
10.支持状态栏文件行数统计、有效报文数统计
11.支持回车键自动查找ID
二、效果图:


三、关键源码:
#include "canparser.h"
#include <QFile>
#include <QTextStream>
#include <QDateTime>
#include <QDebug>
#include <QtConcurrent/QtConcurrentRun>
#include <QElapsedTimer>
#include <QRegularExpression>
// 注册元类型(必须在任何使用该类型的信号槽连接之前)
const static int _ = [](){
qRegisterMetaType<QVector<CANMessage>>("QVector<CANMessage>");
qRegisterMetaType<CANMessage>("CANMessage");
return 0;
}();
// 静态成员初始化
QRegularExpression canparser::busMasterRe(
"^\\s*(\\d{2}):(\\d{2}):(\\d{2}):(\\d{3,4})\\s+"
"(Rx|Tx)\\s+(\\d+)\\s+"
"(0x[0-9A-Fa-f]{1,8}|[0-9A-Fa-f]{1,8})\\s+" // CAN ID
"([xXsS])\\s+" // 帧类型标识(x/X为扩展帧,s/S为标准帧)
"(\\d+)\\s+" // 数据长度
"((?:[0-9A-Fa-f]{2}\\s*)+)\\s*$"); // 数据
QRegularExpression canparser::ascRe(
"^\\s*(\\d+\\.\\d+)\\s+" // 1. 时间戳
"(\\d+)\\s+" // 2. 通道
"([0-9A-F]+)(x)?\\s+" // 3. ID (4. 带x标记)
"(Rx|Tx)\\s+" // 5. 方向
"d\\s+" // 6. 数据标识
"(\\d+)\\s+" // 7. 数据长度
"((?:[0-9A-F]{2}\\s*)+)"); // 8. 数据部分
QRegularExpression canparser::csvRe("^\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}\"?");
canparser::canparser(QObject *parent) : QObject(parent)
{
busMasterRe.optimize();
ascRe.optimize();
csvRe.optimize();
}
canparser::~canparser()
{
cancelParsing();
}
void canparser::cancelParsing()
{
m_cancelFlag.store(true);
}
bool canparser::tryParseTimestamp(const QString &str, qint64 &result)
{
bool ok;
double timestamp = str.toDouble(&ok);
if (ok) {
result = static_cast<qint64>(timestamp * 1e6); // 秒转微秒
return true;
}
QDateTime dt = QDateTime::fromString(str, "yyyy-MM-dd HH:mm:ss.zzz");
if (dt.isValid()) {
result = dt.toMSecsSinceEpoch() * 1000; // 毫秒转微秒
return true;
}
// 支持BusMaster时间格式 (HH:mm:ss:zzz)
QRegularExpression timeRe("^(\\d{2}):(\\d{2}):(\\d{2}):(\\d{3,4})$");
auto match = timeRe.match(str);
if (match.hasMatch()) {
int h = match.captured(1).toInt();
int m = match.captured(2).toInt();
int s = match.captured(3).toInt();
int ms = match.captured(4).leftJustified(3, '0').left(3).toInt();
result = (h * 3600LL + m * 60LL + s) * 1000000LL + ms * 1000LL;
return true;
}
return false;
}
qint64 canparser::parseBusMasterStartTime(const QString &line)
{
QRegularExpression re("(\\d+):(\\d+):(\\d+)\\s+(\\d+):(\\d+):(\\d+):(\\d+)");
QRegularExpressionMatch match = re.match(line);
if (match.hasMatch()) {
int day = match.captured(1).toInt();
int month = match.captured(2).toInt();
int year = match.captured(3).toInt();
int hour = match.captured(4).toInt();
int minute = match.captured(5).toInt();
int second = match.captured(6).toInt();
int msec = match.captured(7).toInt();
QDate date(year, month, day);
QTime time(hour, minute, second, msec);
return QDateTime(date, time).toMSecsSinceEpoch();
}
return QDateTime::currentDateTime().toMSecsSinceEpoch();
}
bool canparser::parseFile(const QString &filePath, QVector<CANMessage> &messages)
{
m_cancelFlag.store(false);
messages.clear();
switch (detectFormat(filePath)) {
case FormatASC: return parseASC(filePath, messages);
case FormatCSV: return parseCSV(filePath, messages);
case FormatBusMaster: return parseBusMaster(filePath, messages);
default:
qWarning() << "Unsupported log format1:" << filePath;
return false;
}
}
void canparser::parseFileAsync(const QString &filePath)
{
m_cancelFlag.store(false);
QtConcurrent::run([this, filePath]() {
QElapsedTimer timer;
timer.start();
QVector<CANMessage> messages;
bool success = false;
LogFormat format = detectFormat(filePath);
qDebug()<< "要解析的文件格式:" << format;
switch(format) {
case FormatASC:
success = parseASCWithProgress(filePath, messages);
break;
case FormatCSV:
success = parseCSVWithProgress(filePath, messages);
break;
case FormatBusMaster:
success = parseBusMasterWithProgress(filePath, messages);
break;
default:
qWarning() << "Unsupported log format2:" << filePath;
}
qDebug()<< "文件解析结果:" << success << !m_cancelFlag.load() << "总行数:"<<totalLineCount;
emit parseFinished(success && !m_cancelFlag.load(), messages,totalLineCount);
});
}
canparser::LogFormat canparser::detectFormat(const QString &filePath)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return FormatUnknown;
}
QTextStream in(&file);
QString line;
bool isBusMaster = false;
bool hasDataHeader = false;
bool isASC = false;
bool isCSV = false;
int ascLineCount = 0;
int csvLineCount = 0;
bool hasCSVHeader = false;
for (int i = 0; i < 20 && !in.atEnd(); ++i) {
line = in.readLine().trimmed();
// 检测BUSMASTER格式
if (line.startsWith("***BUSMASTER")) {
isBusMaster = true;
}
if (line.contains("<Time><Tx/Rx><Channel><CAN ID><Type><DLC><DataBytes>")) {
hasDataHeader = true;
}
if (!line.startsWith("***") && !line.isEmpty()) {
if (busMasterRe.match(line).hasMatch()) {
file.close();
return FormatBusMaster;
}
}
// 检测ASC格式
if (line.startsWith("date ") || line.startsWith("// version ") ||
line.startsWith("// base ") || line.startsWith("begin ") ||
line.startsWith("base ") || line.startsWith("timestamp ")) {
isASC = true;
ascLineCount++;
}
else if (line.contains(QRegExp("^\\d+\\.\\d+\\s+\\w+\\s+\\d+\\s+[RT]x\\s+\\w+\\s+.+"))) {
isASC = true;
ascLineCount++;
}
// 针对您提供的CSV格式的检测
QStringList csvParts = line.split(',');
if (csvParts.size() >= 8) { // 根据您的示例,至少有8个字段
// 检查是否是CSV标题行
if (csvParts.contains("Time") &&
(csvParts.contains("ID") || csvParts.contains("CAN_CHNL")) &&
csvParts.contains("DLC")) {
hasCSVHeader = true;
isCSV = true;
csvLineCount++;
}
// 检查数据行
else {
// 检查时间格式(2025-03-10_21:07:07.429)
QRegExp timeRegex("^\\d{4}-\\d{2}-\\d{2}_\\d{2}:\\d{2}:\\d{2}\\.\\d{3}");
// 检查CAN ID格式(0xCFF03D0或十六进制数)
QRegExp idRegex("^(0x)?[0-9A-Fa-f]+$");
if (timeRegex.exactMatch(csvParts[0].trimmed()) &&
idRegex.exactMatch(csvParts[2].trimmed())) {
csvLineCount++;
}
}
}
}
file.close();
if (isBusMaster && hasDataHeader) {
return FormatBusMaster;
}
// 如果检测到多行ASC特征,优先返回ASC
if (isASC && ascLineCount > 2) {
return FormatASC;
}
// 如果检测到CSV标题行或多行符合CSV数据格式
if (hasCSVHeader || csvLineCount > 2) {
return FormatCSV;
}
return FormatUnknown;
}
bool canparser::parseASC(const QString &filePath, QVector<CANMessage> &messages)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
QTextStream in(&file);
while (!in.atEnd()) {
QString line = in.readLine().trimmed();
QRegularExpressionMatch match = ascRe.match(line);
if (match.hasMatch()) {
CANMessage msg;
if (!tryParseTimestamp(match.captured(1), msg.timestamp)) continue;
msg.id = match.captured(3).toUInt(nullptr, 16);
msg.isTx = false;
QString dataStr = match.captured(4).replace(" ", "");
for (int i = 0; i < dataStr.length(); i += 2) {
msg.data.append(static_cast<char>(dataStr.mid(i, 2).toUInt(nullptr, 16)));
}
messages.append(msg);
}
}
file.close();
return !messages.isEmpty();
}
bool canparser::parseASCWithProgress(const QString &filePath, QVector<CANMessage> &messages)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
return false;
qint64 totalSize = file.size();
qint64 processedSize = 0;
const int batchSize = 1000;
int count = 0;
QTextStream in(&file);
totalLineCount = 0;
qDebug()<< "ASC解析:开始解析";
while (!in.atEnd() && !m_cancelFlag.load()) {
QString line = in.readLine().trimmed();
processedSize += line.size() + 1;
totalLineCount += 1;
// 跳过注释行和文件头
if (line.startsWith("//") || line.startsWith("date") || line.startsWith("base") || line.isEmpty()) {
qDebug()<< "ASC解析:跳过无效行";
continue;
}
// 进度通知
if (++count % batchSize == 0 || processedSize % (1024*1024) == 0) {
emit parseProgress(qMin(static_cast<int>(processedSize * 100 / totalSize), 99));
}
// 正则匹配
QRegularExpressionMatch match = ascRe.match(line);
if (match.hasMatch()) {
CANMessage msg;
// 解析时间戳
if (!tryParseTimestamp(match.captured(1), msg.timestamp)) {
continue;

最低0.47元/天 解锁文章
1万+

被折叠的 条评论
为什么被折叠?



