QT5.13.2 CAN报文周期分析工具

通过这个小软件可以很方便分析周期不稳定的报文,辅助快速解决丢包等问题。

一、功能描述:

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;
  
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值