一、前言:为什么选择这条路?

想象一下,你正在学习自动驾驶技术,但一开始就用真车在高速公路上练习显然不现实。同样地,学习CANoe工程和CI/CD自动化,从Demo版虚拟环境开始是最安全、最经济、最高效的路径

在这份指南中,我将带你:

  1. 零硬件投入的情况下搭建完整的学习环境
  2. 掌握CAPL编程的核心精髓
  3. 构建真实的GitHub CI/CD自动化流程
  4. 创建一个可立即运行的完整示例项目

让我们开始这段令人兴奋的学习之旅吧! 🎯


二、全景视图:我们的学习架构

在深入细节之前,先看看我们将要构建的完整系统架构:

提交代码

触发工作流

执行测试任务

上传结果

本地开发循环

编写CAPL代码

本地验证

提交到GitHub

开发者本地环境

GitHub云端仓库

GitHub Actions

自托管Runner
服务器PC

CANoe Demo环境

虚拟总线通信

CAPL测试脚本

生成测试报告

GitHub PR/Merge请求

三、阶段一:基础环境搭建(第1周)

3.1 获取CANoe Demo版

操作步骤:

  1. 访问Vector官网

    # Vector中国官网(中文支持更好)
    https://www.vector.com/cn/zh/
    
    # 或国际官网
    https://www.vector.com/int/en/
    
  2. 下载Demo版

    • 注册免费账户
    • 进入下载区域,选择"CANoe Demo Version"
    • 下载适用于Windows的64位版本
    • 文件大小约2-3GB
  3. 安装注意事项

    ; 安装前检查清单:
    ; ✅ Windows 10/11 64位系统
    ; ✅ 至少8GB内存(推荐16GB)
    ; ✅ 20GB可用磁盘空间
    ; ✅ 管理员权限运行安装程序
    ; ✅ 关闭所有杀毒软件(临时)
    

3.2 创建最简单的虚拟CANoe工程

让我们创建一个"Hello CANoe"工程:

步骤1:新建工程

# 工程文件结构
HelloCANoe/
├── Config/           # 配置文件目录
├── Databases/        # 数据库文件
├── Log/             # 日志文件
├── Panels/          # 面板文件
└── Test/            # 测试脚本

步骤2:创建虚拟总线配置
在CANoe中执行以下操作:

  1. FileNewCAN 500kBaud
  2. 设置名称为Virtual_CAN_Network
  3. 添加两个网络节点:
    • ECU_Simulator (发送节点)
    • Test_Monitor (接收节点)

步骤3:配置虚拟通道

<!-- 这是CANoe配置文件的结构示意 -->
<Configuration>
  <Channels>
    <CAN>
      <Channel Name="CAN1" BusType="CAN" Bitrate="500" />
    </CAN>
  </Channels>
  <NetworkNodes>
    <Node Name="ECU_Simulator" Channel="CAN1" />
    <Node Name="Test_Monitor" Channel="CAN1" />
  </NetworkNodes>
</Configuration>

3.3 CAPL基础:你的第一个通信程序

创建ECU_Simulator.can文件:

/*!
 * \file ECU_Simulator.can
 * \brief 虚拟ECU模拟器 - 发送周期报文
 * \author 学习者
 * \date 创建日期
 * 
 * 这个CAPL脚本模拟一个简单的ECU,周期发送引擎状态报文。
 * 学习要点:
 * 1. 变量声明
 * 2. 定时器使用
 * 3. 报文发送
 * 4. 事件处理
 */

/* 全局变量声明区 */
variables
{
  // 消息定义
  message EngineMsg msgEngineData = {dlc = 8, id = 0x100};  // 引擎数据报文
  
  // 信号变量
  int engineSpeed = 0;        // 引擎转速 (rpm)
  byte coolantTemp = 90;      // 冷却液温度 (°C)
  byte throttlePosition = 0;  // 节气门位置 (%)
  
  // 定时器
  msTimer timer100ms;         // 100ms定时器
  msTimer timer1s;            // 1秒定时器
  
  // 计数器
  long messageCount = 0;      // 发送报文计数
}

/*!
 * \brief 程序启动事件
 * 
 * 当CANoe开始测量时自动触发
 * 用于初始化变量和启动定时器
 */
on start
{
  write("====================================");
  write("  虚拟ECU模拟器启动");
  write("  版本: 1.0");
  write("  时间: %s", getLocalTimeString());
  write("====================================");
  
  // 初始化报文ID和DLC
  msgEngineData.id = 0x100;    // 标准CAN ID
  msgEngineData.dlc = 8;       // 数据长度8字节
  
  // 启动定时器
  setTimer(timer100ms, 100);  // 每100ms执行一次
  setTimer(timer1s, 1000);    // 每1秒执行一次
  
  write("定时器已启动: 100ms周期, 1s周期");
}

/*!
 * \brief 100ms定时器事件
 * 
 * 周期发送引擎数据报文
 * 模拟实时数据更新
 */
on timer timer100ms
{
  // 模拟引擎转速变化 (800-3000 RPM)
  engineSpeed = 800 + (messageCount % 22) * 100;
  if (engineSpeed > 3000) engineSpeed = 800;
  
  // 模拟温度变化 (85-105°C)
  coolantTemp = 85 + (messageCount % 21);
  
  // 模拟节气门位置 (0-100%)
  throttlePosition = (messageCount * 7) % 101;
  
  // 填充报文数据
  msgEngineData.byte(0) = (engineSpeed >> 8) & 0xFF;  // 转速高字节
  msgEngineData.byte(1) = engineSpeed & 0xFF;         // 转速低字节
  msgEngineData.byte(2) = coolantTemp;                // 冷却液温度
  msgEngineData.byte(3) = throttlePosition;           // 节气门位置
  msgEngineData.byte(4) = 0x01;                       // 状态位1: 引擎运行
  msgEngineData.byte(5) = 0x00;                       // 保留
  msgEngineData.byte(6) = messageCount & 0xFF;        // 序列号低字节
  msgEngineData.byte(7) = (messageCount >> 8) & 0xFF; // 序列号高字节
  
  // 发送报文
  output(msgEngineData);
  messageCount++;
  
  // 每10条报文打印一次状态
  if (messageCount % 10 == 0) {
    write("[周期发送] 报文#%d | 转速:%d RPM | 温度:%d°C | 节气门:%d%%", 
          messageCount, engineSpeed, coolantTemp, throttlePosition);
  }
  
  // 重新启动定时器
  setTimer(timer100ms, 100);
}

/*!
 * \brief 1秒定时器事件
 * 
 * 执行周期性任务,如状态报告
 */
on timer timer1s
{
  static int secondCounter = 0;
  secondCounter++;
  
  // 每秒报告状态
  write("[状态报告] 运行时间: %d秒 | 总发送报文: %d", 
        secondCounter, messageCount);
  
  // 重新启动定时器
  setTimer(timer1s, 1000);
}

/*!
 * \brief 键盘事件处理
 * 
 * 支持手动触发功能,便于测试
 */
on key 't'  // 按下t键触发测试
{
  write("=== 手动测试触发 ===");
  write("当前状态:");
  write("  引擎转速: %d RPM", engineSpeed);
  write("  冷却温度: %d °C", coolantTemp);
  write("  节气门位置: %d %%", throttlePosition);
  write("  发送报文数: %d", messageCount);
  write("===================");
}

/*!
 * \brief 收到诊断请求的处理
 * 
 * 模拟ECU对诊断请求的响应
 */
on message 0x7E0  // 标准诊断请求ID
{
  byte requestService = this.byte(1);  // 获取服务ID
  
  write("收到诊断请求: 服务ID 0x%02X", requestService);
  
  // 根据服务ID响应
  if (requestService == 0x01) {
    // 读取当前数据
    write("响应诊断服务: 读取当前数据");
    // 实际项目中这里会发送响应报文
  }
}

/*!
 * \brief 程序停止事件
 * 
 * 当CANoe停止测量时触发
 * 用于清理和总结
 */
on stopMeasurement
{
  write("====================================");
  write("  虚拟ECU模拟器停止");
  write("  最终统计:");
  write("  总发送报文: %d", messageCount);
  write("  总运行时间: %.1f秒", timeNow() / 1000.0);
  write("====================================");
}

创建Test_Monitor.can文件:

/*!
 * \file Test_Monitor.can
 * \brief 测试监控节点 - 接收并验证报文
 * \author 学习者
 * \date 创建日期
 * 
 * 这个CAPL脚本监控总线上的报文,进行基本验证和统计。
 * 学习要点:
 * 1. 报文接收处理
 * 2. 信号提取
 * 3. 简单验证逻辑
 * 4. 错误检测
 */

variables
{
  // 统计变量
  long receivedCount = 0;           // 接收到的报文计数
  long errorCount = 0;              // 错误计数
  long lastSequence = -1;           // 上一次序列号
  msTimer monitorTimer;             // 监控定时器
  
  // 状态标志
  int isCommunicationOK = 0;        // 通信状态
  double lastMessageTime = 0;       // 最后报文时间
}

/*!
 * \brief 程序启动事件
 */
on start
{
  write("====================================");
  write("  测试监控节点启动");
  write("  准备监控ID 0x100的报文...");
  write("====================================");
  
  // 启动监控定时器(每秒检查一次)
  setTimer(monitorTimer, 1000);
}

/*!
 * \brief 引擎报文接收事件
 * 
 * 当收到ID为0x100的报文时触发
 */
on message EngineMsg  // 假设已定义EngineMsg为ID 0x100
{
  receivedCount++;
  double currentTime = timeNow();
  
  // 提取信号
  int engineSpeed = (this.byte(0) << 8) | this.byte(1);
  byte coolantTemp = this.byte(2);
  long sequenceNum = (this.byte(7) << 8) | this.byte(6);
  
  // 检查序列号连续性
  if (lastSequence != -1 && sequenceNum != (lastSequence + 1) % 65536) {
    write("[错误] 序列号不连续: 期望 %d, 收到 %d", 
          (lastSequence + 1) % 65536, sequenceNum);
    errorCount++;
  }
  lastSequence = sequenceNum;
  
  // 检查数据合理性
  if (engineSpeed < 0 || engineSpeed > 10000) {
    write("[警告] 异常转速值: %d RPM", engineSpeed);
  }
  
  if (coolantTemp < 50 || coolantTemp > 130) {
    write("[警告] 异常温度值: %d °C", coolantTemp);
  }
  
  // 更新最后报文时间
  lastMessageTime = currentTime;
  
  // 每20条报文显示一次
  if (receivedCount % 20 == 0) {
    write("[监控] 已接收 %d 条报文 | 错误数: %d | 当前转速: %d RPM", 
          receivedCount, errorCount, engineSpeed);
  }
}

/*!
 * \brief 监控定时器事件
 * 
 * 定期检查通信状态
 */
on timer monitorTimer
{
  double currentTime = timeNow();
  double timeSinceLastMsg = currentTime - lastMessageTime;
  
  // 检查是否超时(超过1秒没收到报文)
  if (lastMessageTime > 0 && timeSinceLastMsg > 1000) {
    write("[错误] 报文接收超时! 已 %.1f 秒未收到报文", timeSinceLastMsg/1000);
    isCommunicationOK = 0;
    errorCount++;
  } else if (lastMessageTime > 0) {
    isCommunicationOK = 1;
  }
  
  // 重新启动定时器
  setTimer(monitorTimer, 1000);
}

/*!
 * \brief 键盘事件 - 手动检查
 */
on key 'c'
{
  write("=== 通信状态检查 ===");
  write("接收报文总数: %d", receivedCount);
  write("检测错误数: %d", errorCount);
  write("通信状态: %s", isCommunicationOK ? "正常" : "异常");
  
  if (lastMessageTime > 0) {
    double timeSinceLast = timeNow() - lastMessageTime;
    write("距离最后报文: %.1f 毫秒", timeSinceLast);
  } else {
    write("尚未收到任何报文");
  }
  write("===================");
}

四、阶段二:GitHub CI/CD环境搭建(第2周)

4.1 设置GitHub仓库

步骤1:创建新仓库

# 在GitHub网页操作或使用命令行
git init
git add .
git commit -m "初始化CANoe学习项目"
git branch -M main
git remote add origin https://github.com/你的用户名/canoe-learning.git
git push -u origin main

4.2 配置自托管GitHub Runner

步骤1:在服务器PC上设置Runner

# 1. 下载Runner
# 访问 https://github.com/你的用户名/canoe-learning/settings/actions/runners
# 点击 "New self-hosted runner"
# 选择Windows x64版本

# 2. 解压到C:\actions-runner
mkdir C:\actions-runner
cd C:\actions-runner

# 3. 配置Runner(以管理员权限运行PowerShell)
.\config.cmd --url https://github.com/你的用户名/canoe-learning --token 你的token

# 配置选项示例:
# Runner名称: canoe-test-server
# 工作目录: _work
# 标签: canoe, windows, self-hosted
# 作为服务运行: 是

# 4. 安装为服务
.\svc install
.\svc start

# 5. 验证Runner状态
.\run.cmd --check

步骤2:创建Runner配置脚本

# scripts/setup-runner.ps1
# CANoe CI/CD Runner安装和配置脚本

param(
    [string]$GitHubToken,
    [string]$RepositoryUrl = "https://github.com/YOUR_USERNAME/canoe-learning",
    [string]$RunnerName = "canoe-test-server-$env:COMPUTERNAME"
)

Write-Host "========================================" -ForegroundColor Cyan
Write-Host "  GitHub自托管Runner配置脚本" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

# 检查管理员权限
if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
    Write-Host "错误: 请以管理员身份运行此脚本!" -ForegroundColor Red
    exit 1
}

# 检查GitHub Token
if ([string]::IsNullOrEmpty($GitHubToken)) {
    Write-Host "错误: 必须提供GitHub Token!" -ForegroundColor Red
    Write-Host "使用方法: .\setup-runner.ps1 -GitHubToken YOUR_TOKEN" -ForegroundColor Yellow
    exit 1
}

# 1. 创建Runner目录
$RunnerDir = "C:\ActionsRunner"
if (Test-Path $RunnerDir) {
    Write-Host "Runner目录已存在,先清理..." -ForegroundColor Yellow
    Remove-Item -Path $RunnerDir -Recurse -Force
}

New-Item -ItemType Directory -Path $RunnerDir -Force | Out-Null
Set-Location $RunnerDir

Write-Host "创建Runner目录: $RunnerDir" -ForegroundColor Green

# 2. 下载最新Runner
$RunnerUrl = "https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-win-x64-2.311.0.zip"
$RunnerZip = "actions-runner-win-x64.zip"

Write-Host "下载GitHub Runner..." -ForegroundColor Cyan
Invoke-WebRequest -Uri $RunnerUrl -OutFile $RunnerZip

# 3. 解压Runner
Write-Host "解压Runner文件..." -ForegroundColor Cyan
Expand-Archive -Path $RunnerZip -DestinationPath . -Force

# 4. 配置Runner
Write-Host "配置Runner..." -ForegroundColor Cyan
.\config.cmd --unattended --url $RepositoryUrl --token $GitHubToken --name $RunnerName --labels "canoe,windows,self-hosted" --replace

# 5. 安装为Windows服务
Write-Host "安装为Windows服务..." -ForegroundColor Cyan
.\svc install

# 6. 启动服务
Write-Host "启动Runner服务..." -ForegroundColor Cyan
.\svc start

# 7. 创建环境检查脚本
$EnvCheckScript = @"
@echo off
echo ========================================
echo  CANoe CI/CD环境检查
echo ========================================

REM 检查CANoe安装
where CANoe64.exe >nul 2>&1
if %errorlevel% equ 0 (
    echo [OK] CANoe已安装
    CANoe64.exe /Version
) else (
    echo [ERROR] CANoe未找到
)

REM 检查Python
python --version >nul 2>&1
if %errorlevel% equ 0 (
    echo [OK] Python已安装
) else (
    echo [WARNING] Python未安装
)

REM 检查网络连通性
ping -n 1 github.com >nul 2>&1
if %errorlevel% equ 0 (
    echo [OK] 网络连接正常
) else (
    echo [ERROR] 无法连接到GitHub
)

REM 检查磁盘空间
for /f "tokens=3" %%a in ('dir /-c ^| find "可用字节"') do set free=%%a
echo [INFO] 可用磁盘空间: %free% 字节

echo ========================================
"@

Set-Content -Path "$RunnerDir\check_env.bat" -Value $EnvCheckScript

Write-Host "`n========================================" -ForegroundColor Green
Write-Host "  Runner配置完成!" -ForegroundColor Green
Write-Host "  Runner名称: $RunnerName" -ForegroundColor Green
Write-Host "  标签: canoe, windows, self-hosted" -ForegroundColor Green
Write-Host "  状态检查: $RunnerDir\check_env.bat" -ForegroundColor Green
Write-Host "========================================" -ForegroundColor Green

# 运行环境检查
Write-Host "`n执行环境检查..." -ForegroundColor Cyan
& "$RunnerDir\check_env.bat"

4.3 创建自动化构建和测试脚本

创建批处理脚本:

:: scripts/run_canoe_test.bat
:: CANoe自动化测试脚本 - 用于GitHub Actions CI/CD

@echo off
setlocal enabledelayedexpansion

echo ========================================
echo  CANoe自动化测试启动
echo  时间: %date% %time%
echo ========================================

:: 设置路径
set CANOE_PATH="C:\Program Files\Vector\CANoe\Exec64\CANoe64.exe"
set WORKSPACE=%CD%
set CONFIG_FILE="%WORKSPACE%\config\demo_config.cfg"
set RESULTS_DIR="%WORKSPACE%\results\%DATE:/=-%_%TIME::=-%"
set LOG_FILE="%RESULTS_DIR%\canoe_test.log"

:: 创建结果目录
if not exist "%RESULTS_DIR%" mkdir "%RESULTS_DIR%"

:: 检查CANoe是否安装
if not exist %CANOE_PATH% (
    echo [错误] 未找到CANoe安装: %CANOE_PATH%
    echo 请检查CANoe是否正确安装
    exit /b 1
)

:: 检查配置文件是否存在
if not exist %CONFIG_FILE% (
    echo [错误] 未找到配置文件: %CONFIG_FILE%
    echo 请确保配置文件存在于config目录
    exit /b 1
)

:: 记录环境信息
echo [信息] CANoe路径: %CANOE_PATH% > "%LOG_FILE%"
echo [信息] 配置文件: %CONFIG_FILE% >> "%LOG_FILE%"
echo [信息] 工作目录: %WORKSPACE% >> "%LOG_FILE%"
echo [信息] 结果目录: %RESULTS_DIR% >> "%LOG_FILE%"

:: 获取CANoe版本信息
echo. >> "%LOG_FILE%"
echo [信息] CANoe版本信息: >> "%LOG_FILE%"
%CANOE_PATH% /Version >> "%LOG_FILE%" 2>&1

:: 创建临时测试配置文件
echo 创建临时测试配置...
set TEMP_CONFIG="%RESULTS_DIR%\temp_config.cfg"

(
echo ; ========================================
echo ;  自动生成的CANoe测试配置
echo ;  生成时间: %date% %time%
echo ; ========================================
echo ;
echo [GENERAL]
echo Version=1.0
echo Description=Demo Configuration for CI/CD Testing
echo ;
echo [CAN]
echo ChannelCount=1
echo ;
echo [Channel1]
echo Name=CAN1
echo Driver=Virtual
echo Baudrate=500
echo ;
echo [Nodes]
echo Node1=ECU_Simulator
echo Node2=Test_Monitor
echo ;
echo [CAPL]
echo ECU_Simulator=..\src\ECU_Simulator.can
echo Test_Monitor=..\src\Test_Monitor.can
echo ;
echo [Test]
echo Timeout=120000
) > %TEMP_CONFIG%

:: 运行CANoe测试
echo ======================================== >> "%LOG_FILE%"
echo 开始运行CANoe测试... >> "%LOG_FILE%"
echo 开始时间: %time% >> "%LOG_FILE%"

echo [信息] 启动CANoe测试...
echo [信息] 注意: Demo版将在30分钟后自动停止

:: 使用命令行运行CANoe(无界面模式)
set START_TIME=%time%
%CANOE_PATH% /configuration %TEMP_CONFIG% /measurement /start /logging "%RESULTS_DIR%\trace.asc" /report "%RESULTS_DIR%\report.xml" /quit

set EXIT_CODE=%errorlevel%
set END_TIME=%time%

echo [信息] CANoe测试完成 >> "%LOG_FILE%"
echo [信息] 退出代码: %EXIT_CODE% >> "%LOG_FILE%"
echo [信息] 开始时间: %START_TIME% >> "%LOG_FILE%"
echo [信息] 结束时间: %END_TIME% >> "%LOG_FILE%"

:: 分析测试结果
if %EXIT_CODE% EQU 0 (
    echo [成功] CANoe测试执行完成
    echo [成功] 测试结果保存在: %RESULTS_DIR%
    
    :: 生成简单的测试摘要
    call :GenerateTestSummary "%RESULTS_DIR%"
    
    exit /b 0
) else (
    echo [失败] CANoe测试执行失败,退出代码: %EXIT_CODE%
    echo [失败] 请检查日志文件: %LOG_FILE%
    
    exit /b %EXIT_CODE%
)

:: ========================================
:: 函数: 生成测试摘要
:: ========================================
:GenerateTestSummary
set SUMMARY_DIR=%~1
set SUMMARY_FILE="%SUMMARY_DIR%\test_summary.md"

(
echo # CANoe测试执行摘要
echo.
echo ## 测试信息
echo - **执行时间**: %DATE% %TIME%
echo - **配置文件**: %CONFIG_FILE%
echo - **结果目录**: %SUMMARY_DIR%
echo - **执行状态**: %EXIT_CODE%
echo.
echo ## 系统信息
echo - **CANoe版本**: 请查看version.txt
echo - **工作目录**: %WORKSPACE%
echo - **执行耗时**: %START_TIME% - %END_TIME%
echo.
echo ## 生成的文件
echo 以下文件已生成:
echo 1. `trace.asc` - 总线跟踪数据
echo 2. `report.xml` - 测试报告(如果配置了Test Unit)
echo 3. `canoe_test.log` - 执行日志
echo 4. `temp_config.cfg` - 使用的临时配置
echo.
echo ## 后续步骤
echo 1. 检查trace.asc文件分析通信数据
echo 2. 查看日志文件定位问题
echo 3. 修改CAPL脚本并重新提交测试
echo.
echo ---
echo *自动生成于 %DATE% %TIME%*
) > %SUMMARY_FILE%

echo [信息] 测试摘要已生成: %SUMMARY_FILE%
goto :eof

:: ========================================
:: 错误处理
:: ========================================
:ErrorHandler
echo [错误] 脚本执行出错,错误级别: %errorlevel%
echo [错误] 最后执行的命令: %cmdcmdline%
exit /b 1

endlocal

创建Python辅助脚本:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
CANoe CI/CD自动化工具
文件名: canoe_cicd_tool.py
描述: 用于GitHub Actions中自动化运行和验证CANoe测试的工具
作者: 学习者
版本: 1.0.0
"""

import os
import sys
import time
import subprocess
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path
import json
import logging
from typing import Dict, List, Optional, Tuple

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('canoe_cicd.log'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class CanoeCICDManager:
    """CANoe CI/CD管理类"""
    
    def __init__(self, workspace: str, canoe_path: str = None):
        """
        初始化CANoe CI/CD管理器
        
        Args:
            workspace: 工作空间路径
            canoe_path: CANoe可执行文件路径(如果为None则自动查找)
        """
        self.workspace = Path(workspace).absolute()
        self.canoe_path = canoe_path or self._find_canoe()
        self.results_dir = self.workspace / 'results' / f"run_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
        self.configs_dir = self.workspace / 'configs'
        self.src_dir = self.workspace / 'src'
        
        # 创建必要的目录
        self.results_dir.mkdir(parents=True, exist_ok=True)
        self.configs_dir.mkdir(parents=True, exist_ok=True)
        
        logger.info(f"初始化CANoe CI/CD管理器")
        logger.info(f"工作空间: {self.workspace}")
        logger.info(f"CANoe路径: {self.canoe_path}")
        logger.info(f"结果目录: {self.results_dir}")
    
    def _find_canoe(self) -> Optional[Path]:
        """查找CANoe安装路径"""
        possible_paths = [
            Path("C:/Program Files/Vector/CANoe/Exec64/CANoe64.exe"),
            Path("C:/Program Files (x86)/Vector/CANoe/Exec/CANoe.exe"),
            Path(os.environ.get("CANOE_PATH", "")),
        ]
        
        for path in possible_paths:
            if path and path.exists():
                logger.info(f"找到CANoe: {path}")
                return path
        
        logger.error("未找到CANoe安装,请确保已正确安装CANoe")
        return None
    
    def create_test_config(self, config_name: str = "ci_test") -> Path:
        """
        创建测试配置文件
        
        Args:
            config_name: 配置名称
            
        Returns:
            配置文件的Path对象
        """
        config_file = self.configs_dir / f"{config_name}.cfg"
        
        # CAPL文件列表
        capl_files = list(self.src_dir.glob("*.can"))
        capl_nodes = []
        
        for i, capl_file in enumerate(capl_files, 1):
            node_name = capl_file.stem
            relative_path = capl_file.relative_to(self.workspace)
            capl_nodes.append((node_name, str(relative_path).replace('\\', '/')))
        
        # 生成配置内容
        config_content = f"""; ========================================
;  CANoe CI/CD测试配置文件
;  生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}
;  配置名称: {config_name}
; ========================================

[GENERAL]
Version=1.0
Description=Automatically generated config for CI/CD testing
Author=CANoe CI/CD Tool

[CAN]
ChannelCount=1

[Channel1]
Name=CAN1
Driver=Virtual
Baudrate=500

[Nodes]
"""
        
        # 添加节点
        for i, (node_name, _) in enumerate(capl_nodes, 1):
            config_content += f"Node{i}={node_name}\n"
        
        config_content += "\n[CAPL]\n"
        
        # 添加CAPL文件映射
        for node_name, capl_path in capl_nodes:
            config_content += f"{node_name}={capl_path}\n"
        
        config_content += f"""
[Measurement]
StartAutomatically=Yes
StopOnError=No
Timeout=300000  ; 5分钟超时

[Logging]
Enabled=Yes
File={str((self.results_dir / 'trace.asc').relative_to(self.workspace)).replace('\\', '/')}
Format=ASC

[Report]
Enabled=Yes
File={str((self.results_dir / 'report.html').relative_to(self.workspace)).replace('\\', '/')}
Template=Standard
"""
        
        # 写入配置文件
        config_file.write_text(config_content, encoding='utf-8')
        logger.info(f"创建配置文件: {config_file}")
        
        return config_file
    
    def run_canoe_test(self, config_file: Path, timeout: int = 180) -> Dict:
        """
        运行CANoe测试
        
        Args:
            config_file: 配置文件路径
            timeout: 超时时间(秒)
            
        Returns:
            包含执行结果的字典
        """
        if not self.canoe_path:
            raise FileNotFoundError("CANoe路径未找到")
        
        if not config_file.exists():
            raise FileNotFoundError(f"配置文件不存在: {config_file}")
        
        # 构建命令行参数
        cmd = [
            str(self.canoe_path),
            '/configuration', str(config_file),
            '/measurement',
            '/start',
            '/logging', str(self.results_dir / 'trace.asc'),
            '/report', str(self.results_dir / 'report.xml'),
            '/quit'  # 测试完成后自动退出
        ]
        
        logger.info(f"执行CANoe命令: {' '.join(cmd)}")
        
        start_time = time.time()
        
        try:
            # 执行CANoe
            result = subprocess.run(
                cmd,
                cwd=self.workspace,
                timeout=timeout,
                capture_output=True,
                text=True,
                encoding='utf-8',
                errors='ignore'
            )
            
            end_time = time.time()
            duration = end_time - start_time
            
            # 保存输出
            stdout_file = self.results_dir / 'canoe_stdout.txt'
            stderr_file = self.results_dir / 'canoe_stderr.txt'
            
            stdout_file.write_text(result.stdout, encoding='utf-8')
            stderr_file.write_text(result.stderr, encoding='utf-8')
            
            # 解析结果
            test_result = {
                'success': result.returncode == 0,
                'returncode': result.returncode,
                'duration': duration,
                'stdout': str(stdout_file),
                'stderr': str(stderr_file),
                'timestamp': datetime.now().isoformat(),
                'config_file': str(config_file),
                'results_dir': str(self.results_dir)
            }
            
            # 尝试解析XML报告(如果有)
            report_file = self.results_dir / 'report.xml'
            if report_file.exists():
                try:
                    test_result['report'] = self._parse_report(report_file)
                except Exception as e:
                    logger.warning(f"解析报告文件失败: {e}")
            
            logger.info(f"CANoe测试完成: {'成功' if test_result['success'] else '失败'}")
            logger.info(f"执行时间: {duration:.2f}秒")
            logger.info(f"退出代码: {result.returncode}")
            
            return test_result
            
        except subprocess.TimeoutExpired:
            logger.error(f"CANoe测试超时 (超过{timeout}秒)")
            return {
                'success': False,
                'returncode': -1,
                'duration': timeout,
                'timeout': True,
                'error': f'Timeout after {timeout} seconds',
                'timestamp': datetime.now().isoformat()
            }
        except Exception as e:
            logger.error(f"CANoe执行异常: {e}")
            return {
                'success': False,
                'returncode': -1,
                'duration': time.time() - start_time,
                'error': str(e),
                'timestamp': datetime.now().isoformat()
            }
    
    def _parse_report(self, report_file: Path) -> Dict:
        """解析XML报告文件"""
        try:
            tree = ET.parse(report_file)
            root = tree.getroot()
            
            report_data = {
                'test_cases': [],
                'summary': {}
            }
            
            # 这里根据实际的XML结构进行解析
            # 示例:查找测试用例
            for testcase in root.findall('.//TestCase'):
                case_data = {
                    'name': testcase.get('name', ''),
                    'result': testcase.get('result', ''),
                    'duration': testcase.get('duration', '')
                }
                report_data['test_cases'].append(case_data)
            
            # 查找摘要信息
            summary = root.find('.//Summary')
            if summary is not None:
                report_data['summary'] = {
                    'total': summary.get('total', '0'),
                    'passed': summary.get('passed', '0'),
                    'failed': summary.get('failed', '0')
                }
            
            return report_data
        except Exception as e:
            logger.error(f"解析报告失败: {e}")
            return {'error': str(e)}
    
    def generate_junit_report(self, test_result: Dict) -> Path:
        """生成JUnit格式的报告(供GitHub Actions显示)"""
        junit_file = self.results_dir / 'junit.xml'
        
        testsuite = ET.Element('testsuite')
        testsuite.set('name', 'CANoe CI/CD Tests')
        testsuite.set('tests', '1')
        testsuite.set('failures', '0' if test_result['success'] else '1')
        testsuite.set('time', str(test_result.get('duration', 0)))
        
        testcase = ET.SubElement(testsuite, 'testcase')
        testcase.set('name', 'CANoe自动化测试')
        testcase.set('time', str(test_result.get('duration', 0)))
        
        if not test_result['success']:
            failure = ET.SubElement(testcase, 'failure')
            failure.set('message', f"测试失败,退出代码: {test_result['returncode']}")
            failure.text = test_result.get('error', 'Unknown error')
            
            # 添加标准错误输出
            system_err = ET.SubElement(testcase, 'system-err')
            if os.path.exists(test_result.get('stderr', '')):
                with open(test_result['stderr'], 'r', encoding='utf-8') as f:
                    system_err.text = f.read()
        
        # 添加系统输出
        system_out = ET.SubElement(testcase, 'system-out')
        system_out.text = f"CANoe测试执行完成\n"
        system_out.text += f"配置文件: {test_result.get('config_file', 'N/A')}\n"
        system_out.text += f"结果目录: {test_result.get('results_dir', 'N/A')}\n"
        
        # 写入文件
        tree = ET.ElementTree(testsuite)
        tree.write(junit_file, encoding='utf-8', xml_declaration=True)
        
        logger.info(f"生成JUnit报告: {junit_file}")
        return junit_file
    
    def generate_summary(self, test_result: Dict) -> Path:
        """生成测试摘要Markdown文件"""
        summary_file = self.results_dir / 'test_summary.md'
        
        success_icon = '✅' if test_result['success'] else '❌'
        status_text = '通过' if test_result['success'] else '失败'
        
        summary_content = f"""# CANoe CI/CD测试摘要

## 测试概览
- **状态**: {success_icon} {status_text}
- **执行时间**: {test_result['timestamp']}
- **持续时间**: {test_result.get('duration', 0):.2f}秒
- **退出代码**: {test_result.get('returncode', 'N/A')}

## 配置信息
- **配置文件**: `{test_result.get('config_file', 'N/A')}`
- **工作空间**: `{self.workspace}`
- **结果目录**: `{test_result.get('results_dir', 'N/A')}`

## 生成的文件
"""

        # 列出生成的文件
        if os.path.exists(self.results_dir):
            for file in sorted(self.results_dir.iterdir()):
                if file.is_file():
                    file_size = file.stat().st_size
                    summary_content += f"- `{file.name}` ({file_size:,} bytes)\n"
        
        summary_content += f"""
## 详细日志
- [CANoe标准输出]({test_result.get('stdout', 'N/A')})
- [CANoe标准错误]({test_result.get('stderr', 'N/A')})

## 后续步骤
1. 检查测试结果文件
2. 分析trace.asc中的总线数据
3. 根据需要修改CAPL脚本
4. 重新提交触发新的测试

---
*本报告由CANoe CI/CD工具自动生成*
"""
        
        summary_file.write_text(summary_content, encoding='utf-8')
        logger.info(f"生成测试摘要: {summary_file}")
        
        return summary_file

def main():
    """主函数"""
    import argparse
    
    parser = argparse.ArgumentParser(description='CANoe CI/CD自动化工具')
    parser.add_argument('--workspace', default='.', help='工作空间路径')
    parser.add_argument('--config', help='配置文件名称')
    parser.add_argument('--timeout', type=int, default=300, help='测试超时时间(秒)')
    
    args = parser.parse_args()
    
    # 创建管理器
    manager = CanoeCICDManager(args.workspace)
    
    if not manager.canoe_path:
        logger.error("CANoe未找到,请确保已正确安装")
        sys.exit(1)
    
    # 创建配置
    config_name = args.config or 'ci_test'
    config_file = manager.create_test_config(config_name)
    
    # 运行测试
    logger.info("开始运行CANoe测试...")
    test_result = manager.run_canoe_test(config_file, args.timeout)
    
    # 生成报告
    junit_report = manager.generate_junit_report(test_result)
    summary = manager.generate_summary(test_result)
    
    # 输出结果
    print(f"\n{'='*60}")
    print(f"CANoe CI/CD测试完成")
    print(f"{'='*60}")
    print(f"状态: {'✅ 通过' if test_result['success'] else '❌ 失败'}")
    print(f"耗时: {test_result.get('duration', 0):.2f}秒")
    print(f"结果目录: {manager.results_dir}")
    print(f"JUnit报告: {junit_report}")
    print(f"测试摘要: {summary}")
    print(f"{'='*60}")
    
    # 返回退出代码
    sys.exit(0 if test_result['success'] else 1)

if __name__ == '__main__':
    main()

创建Makefile(用于完整构建流程):

# Makefile for CANoe CI/CD Learning Project
# 版本: 1.0.0
# 描述: 自动化构建和测试脚本

# ========================================
# 配置区域
# ========================================
PROJECT_NAME = canoe-learning
VERSION = 1.0.0

# 目录配置
SRC_DIR = src
CONFIG_DIR = config
SCRIPTS_DIR = scripts
BUILD_DIR = build
RESULTS_DIR = results
DOCS_DIR = docs

# 工具路径
PYTHON = python
GIT = git
CANOE_PATH = "C:\Program Files\Vector\CANoe\Exec64\CANoe64.exe"

# 颜色定义
RED = \033[0;31m
GREEN = \033[0;32m
YELLOW = \033[1;33m
BLUE = \033[0;34m
NC = \033[0m # No Color

# ========================================
# 帮助信息
# ========================================
.PHONY: help
help:
	@echo "$(BLUE)$(PROJECT_NAME) 构建系统 v$(VERSION)$(NC)"
	@echo "使用: make [目标]"
	@echo ""
	@echo "目标:"
	@echo "  $(GREEN)help$(NC)      - 显示此帮助信息"
	@echo "  $(GREEN)all$(NC)       - 执行完整构建流程"
	@echo "  $(GREEN)clean$(NC)     - 清理构建文件"
	@echo "  $(GREEN)setup$(NC)     - 设置开发环境"
	@echo "  $(GREEN)test$(NC)      - 运行CANoe测试"
	@echo "  $(GREEN)check$(NC)     - 代码检查"
	@echo "  $(GREEN)package$(NC)   - 打包项目"
	@echo "  $(GREEN)docs$(NC)      - 生成文档"
	@echo "  $(GREEN)ci-test$(NC)   - CI/CD测试"

# ========================================
# 环境设置
# ========================================
.PHONY: setup
setup:
	@echo "$(YELLOW)设置开发环境...$(NC)"
	
	# 创建目录结构
	@mkdir -p $(BUILD_DIR)
	@mkdir -p $(RESULTS_DIR)
	@mkdir -p $(DOCS_DIR)
	
	# 检查Python环境
	@echo "$(BLUE)检查Python环境...$(NC)"
	@$(PYTHON) --version || (echo "$(RED)Python未安装!$(NC)" && exit 1)
	
	# 检查CANoe安装
	@echo "$(BLUE)检查CANoe安装...$(NC)"
	@if exist $(CANOE_PATH) ( \
		echo "$(GREEN)找到CANoe: $(CANOE_PATH)$(NC)" \
	) else ( \
		echo "$(RED)未找到CANoe!$(NC)" \
		echo "请确保CANoe已安装在默认路径" \
	)
	
	# 安装Python依赖
	@echo "$(BLUE)安装Python依赖...$(NC)"
	@$(PYTHON) -m pip install --upgrade pip
	@$(PYTHON) -m pip install -r requirements.txt 2>nul || echo "$(YELLOW)没有requirements.txt,跳过依赖安装$(NC)"
	
	@echo "$(GREEN)环境设置完成!$(NC)"

# ========================================
# 代码检查
# ========================================
.PHONY: check
check:
	@echo "$(YELLOW)运行代码检查...$(NC)"
	
	# 检查CAPL文件
	@echo "$(BLUE)检查CAPL文件语法...$(NC)"
	@if exist $(SRC_DIR) ( \
		for %%f in ($(SRC_DIR)\*.can) do ( \
			echo  检查 %%~nxf... && \
			type "%%f" | findstr /B /C:"/*!" > nul && ( \
				echo $(GREEN)  ✓ 包含文档头$(NC) \
			) || ( \
				echo $(RED)  ✗ 缺少文档头$(NC) \
			) \
		) \
	)
	
	# 检查配置文件
	@echo "$(BLUE)检查配置文件...$(NC)"
	@if exist $(CONFIG_DIR) ( \
		for %%f in ($(CONFIG_DIR)\*.cfg) do ( \
			echo  检查 %%~nxf... && \
			echo $(GREEN)  ✓ 配置文件存在$(NC) \
		) \
	)
	
	@echo "$(GREEN)代码检查完成!$(NC)"

# ========================================
# 运行测试
# ========================================
.PHONY: test
test:
	@echo "$(YELLOW)运行CANoe测试...$(NC)"
	
	# 创建临时结果目录
	@set TIMESTAMP=$$(date +"%Y%m%d_%H%M%S") 2>nul || set TIMESTAMP=%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%
	@set TEST_RESULTS_DIR=$(RESULTS_DIR)\test_%TIMESTAMP%
	@mkdir "%TEST_RESULTS_DIR%" 2>nul || echo "创建结果目录"
	
	# 运行CANoe测试
	@echo "$(BLUE)执行CANoe自动化测试...$(NC)"
	@if exist $(SCRIPTS_DIR)\run_canoe_test.bat ( \
		call $(SCRIPTS_DIR)\run_canoe_test.bat \
	) else ( \
		echo "$(RED)未找到测试脚本!$(NC)" && exit 1 \
	)
	
	# 运行Python测试工具
	@echo "$(BLUE)运行Python测试工具...$(NC)"
	@$(PYTHON) $(SCRIPTS_DIR)\canoe_cicd_tool.py --workspace . --timeout 180
	
	@echo "$(GREEN)测试执行完成!$(NC)"
	@echo "$(BLUE)结果保存在: $(TEST_RESULTS_DIR)$(NC)"

# ========================================
# CI/CD测试(模拟GitHub Actions环境)
# ========================================
.PHONY: ci-test
ci-test:
	@echo "$(YELLOW)运行CI/CD测试(模拟GitHub Actions)...$(NC)"
	@echo "$(BLUE)模拟环境变量设置...$(NC)"
	
	# 设置模拟的环境变量
	@set GITHUB_WORKSPACE=%CD%
	@set GITHUB_RUN_ID=local_test
	@set RUNNER_TEMP=%CD%\temp
	@mkdir "%RUNNER_TEMP%" 2>nul
	
	# 执行完整的CI流程
	@echo "$(BLUE)步骤1: 检出代码(模拟)...$(NC)"
	@echo "$(BLUE)步骤2: 设置环境...$(NC)"
	@call make setup
	
	@echo "$(BLUE)步骤3: 代码检查...$(NC)"
	@call make check
	
	@echo "$(BLUE)步骤4: 运行测试...$(NC)"
	@call make test
	
	@echo "$(BLUE)步骤5: 生成报告...$(NC)"
	@$(PYTHON) $(SCRIPTS_DIR)\canoe_cicd_tool.py --workspace . --timeout 120
	
	@echo "$(GREEN)CI/CD测试流程完成!$(NC)"
	@echo "$(BLUE)检查 $(RESULTS_DIR) 目录查看测试结果$(NC)"

# ========================================
# 打包项目
# ========================================
.PHONY: package
package:
	@echo "$(YELLOW)打包项目...$(NC)"
	
	@set PACKAGE_NAME=$(PROJECT_NAME)_v$(VERSION)_%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%
	@set PACKAGE_DIR=$(BUILD_DIR)\$(PACKAGE_NAME)
	
	@echo "$(BLUE)创建包目录: $(PACKAGE_DIR)$(NC)"
	@mkdir "%PACKAGE_DIR%" 2>nul
	@mkdir "%PACKAGE_DIR%\src" 2>nul
	@mkdir "%PACKAGE_DIR%\config" 2>nul
	@mkdir "%PACKAGE_DIR%\scripts" 2>nul
	@mkdir "%PACKAGE_DIR%\docs" 2>nul
	
	# 复制文件
	@echo "$(BLUE)复制源文件...$(NC)"
	@xcopy /Y /E "$(SRC_DIR)\*" "%PACKAGE_DIR%\src\" >nul 2>&1
	@xcopy /Y /E "$(CONFIG_DIR)\*" "%PACKAGE_DIR%\config\" >nul 2>&1
	@xcopy /Y /E "$(SCRIPTS_DIR)\*" "%PACKAGE_DIR%\scripts\" >nul 2>&1
	@copy Makefile "%PACKAGE_DIR%\" >nul 2>&1
	@copy README.md "%PACKAGE_DIR%\" >nul 2>&1
	
	# 创建版本信息
	@echo "$(BLUE)创建版本信息...$(NC)"
	@echo Project: $(PROJECT_NAME) > "%PACKAGE_DIR%\VERSION.txt"
	@echo Version: $(VERSION) >> "%PACKAGE_DIR%\VERSION.txt"
	@echo Build Date: %DATE% %TIME% >> "%PACKAGE_DIR%\VERSION.txt"
	@echo Git Commit: >> "%PACKAGE_DIR%\VERSION.txt"
	@$(GIT) rev-parse HEAD >> "%PACKAGE_DIR%\VERSION.txt" 2>nul || echo "Not a git repository" >> "%PACKAGE_DIR%\VERSION.txt"
	
	# 打包为ZIP
	@echo "$(BLUE)创建ZIP包...$(NC)"
	@cd $(BUILD_DIR) && powershell "Compress-Archive -Path '$(PACKAGE_NAME)' -DestinationPath '$(PACKAGE_NAME).zip' -Force"
	
	@echo "$(GREEN)打包完成!$(NC)"
	@echo "$(BLUE)包位置: $(BUILD_DIR)\$(PACKAGE_NAME).zip$(NC)"

# ========================================
# 清理
# ========================================
.PHONY: clean
clean:
	@echo "$(YELLOW)清理构建文件...$(NC)"
	
	@if exist $(BUILD_DIR) ( \
		echo "$(BLUE)删除构建目录...$(NC)" && \
		rmdir /S /Q $(BUILD_DIR) 2>nul \
	)
	
	@if exist $(RESULTS_DIR) ( \
		echo "$(BLUE)清理结果目录...$(NC)" && \
		for /d %%d in ($(RESULTS_DIR)\*) do rmdir /S /Q "%%d" 2>nul && \
		del /Q $(RESULTS_DIR)\* 2>nul \
	)
	
	@echo "$(GREEN)清理完成!$(NC)"

# ========================================
# 完整构建流程
# ========================================
.PHONY: all
all: clean setup check test package
	@echo "$(GREEN)完整构建流程完成!$(NC)"
	@echo "$(BLUE)项目已打包并测试$(NC)"

# ========================================
# 生成文档
# ========================================
.PHONY: docs
docs:
	@echo "$(YELLOW)生成项目文档...$(NC)"
	
	@mkdir -p $(DOCS_DIR)
	
	# 生成项目结构文档
	@echo "$(BLUE)生成项目结构文档...$(NC)"
	@echo "# $(PROJECT_NAME) 项目结构" > $(DOCS_DIR)/PROJECT_STRUCTURE.md
	@echo "" >> $(DOCS_DIR)/PROJECT_STRUCTURE.md
	@echo "\`\`\`" >> $(DOCS_DIR)/PROJECT_STRUCTURE.md
	@tree /F >> $(DOCS_DIR)/PROJECT_STRUCTURE.md 2>nul || echo "安装'tree'命令以查看完整结构" >> $(DOCS_DIR)/PROJECT_STRUCTURE.md
	@echo "\`\`\`" >> $(DOCS_DIR)/PROJECT_STRUCTURE.md
	
	# 生成CAPL API文档(简单版本)
	@echo "$(BLUE)生成CAPL文档...$(NC)"
	@echo "# CAPL脚本文档" > $(DOCS_DIR)/CAPL_DOCUMENTATION.md
	@echo "" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md
	
	@if exist $(SRC_DIR) ( \
		for %%f in ($(SRC_DIR)\*.can) do ( \
			echo ## %%~nxf >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md && \
			echo "" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md && \
			echo "**文件**: \`%%~nxf\`" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md && \
			echo "" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md && \
			findstr /B /C:"/*!" "%%f" | sed "s/\/\*!//g" | sed "s/\*\///g" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md 2>nul || echo "无文档头" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md && \
			echo "" >> $(DOCS_DIR)/CAPL_DOCUMENTATION.md \
		) \
	)
	
	@echo "$(GREEN)文档生成完成!$(NC)"
	@echo "$(BLUE)查看 $(DOCS_DIR) 目录$(NC)"

五、阶段三:GitHub Actions工作流配置(第3-4周)

5.1 创建完整的工作流文件

# .github/workflows/canoe-ci-cd.yml
name: CANoe CI/CD Pipeline

on:
  # 触发条件
  push:
    branches: [ main, develop ]
    paths:
      - 'src/**'
      - 'config/**'
      - 'scripts/**'
      - '.github/workflows/**'
  
  pull_request:
    branches: [ main ]
    types: [opened, synchronize, reopened]
  
  # 手动触发
  workflow_dispatch:
    inputs:
      test_type:
        description: '测试类型'
        required: true
        default: 'full'
        type: choice
        options:
        - quick
        - full
        - integration
      timeout:
        description: '超时时间(秒)'
        required: false
        default: '300'
  
  # 定时触发(每天凌晨运行)
  schedule:
    - cron: '0 2 * * *'  # UTC时间凌晨2点

# 环境变量
env:
  PROJECT_NAME: 'canoe-learning'
  CANOE_CONFIG: 'config/demo_config.cfg'
  PYTHON_VERSION: '3.9'
  
# 作业定义
jobs:
  # 作业1: 代码质量检查
  code-quality:
    name: Code Quality Check
    runs-on: [self-hosted, canoe, windows]
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      with:
        fetch-depth: 0
    
    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: ${{ env.PYTHON_VERSION }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pylint black
        
    - name: Check CAPL file structure
      shell: cmd
      run: |
        echo "检查CAPL文件结构..."
        if exist src\*.can (
          for %%f in (src\*.can) do (
            echo 检查 %%~nxf
            findstr /B /C:"/*!" "%%f" > nul
            if errorlevel 1 (
              echo "::warning file=%%f,title=缺少文档头::文件 %%f 缺少文档头注释"
            )
          )
        )
    
    - name: Run Python linter
      run: |
        echo "运行Python代码检查..."
        python -m pylint scripts/*.py --exit-zero
        
    - name: Check file encoding
      shell: cmd
      run: |
        echo "检查文件编码..."
        chcp 65001
        for /r %%f in (*.can, *.py, *.md) do (
          type "%%f" > nul 2>&1
          if errorlevel 1 (
            echo "::warning file=%%f,title=编码问题::文件 %%f 可能存在编码问题"
          )
        )
  
  # 作业2: 构建和测试
  build-and-test:
    name: Build and Test
    needs: code-quality
    runs-on: [self-hosted, canoe, windows]
    
    # 设置超时
    timeout-minutes: 30
    
    steps:
    - name: Checkout code
      uses: actions/checkout@v3
    
    - name: Display environment info
      shell: cmd
      run: |
        echo "========================================"
        echo "  环境信息"
        echo "========================================"
        ver
        python --version
        git --version
        echo "工作目录: %CD%"
        echo "Runner标签: ${{ runner.labels }}"
        echo "========================================"
    
    - name: Check CANoe installation
      shell: cmd
      run: |
        echo "检查CANoe安装..."
        where CANoe64.exe
        if errorlevel 1 (
          echo "::error::CANoe未安装或不在PATH中"
          exit 1
        )
        
        echo "获取CANoe版本..."
        CANoe64.exe /Version
        echo "CANoe检查完成"
    
    - name: Setup test environment
      shell: cmd
      run: |
        echo "设置测试环境..."
        mkdir -p results
        mkdir -p build
        
        echo "创建环境变量..."
        echo "CANOE_PATH=C:\Program Files\Vector\CANoe\Exec64" >> $env:GITHUB_ENV
        echo "WORKSPACE=%CD%" >> $env:GITHUB_ENV
        
        echo "环境设置完成"
    
    - name: Run CANoe tests
      id: run-tests
      shell: cmd
      run: |
        echo "开始运行CANoe测试..."
        
        set TIMESTAMP=%DATE:~0,4%%DATE:~5,2%%DATE:~8,2%_%TIME:~0,2%%TIME:~3,2%%TIME:~6,2%
        set TIMESTAMP=%TIMESTAMP: =0%
        set RESULTS_DIR=results\test_%TIMESTAMP%
        
        mkdir "%RESULTS_DIR%"
        
        echo "运行主测试脚本..."
        call scripts\run_canoe_test.bat
        
        if errorlevel 1 (
          echo "::error::CANoe测试执行失败"
          echo "失败" >> $env:GITHUB_STEP_SUMMARY
          exit 1
        ) else (
          echo "✅ CANoe测试执行成功"
          echo "成功" >> $env:GITHUB_STEP_SUMMARY
        )
        
        echo "RESULTS_DIR=%RESULTS_DIR%" >> $env:GITHUB_OUTPUT
    
    - name: Run Python test tool
      if: always()
      shell: cmd
      run: |
        echo "运行Python测试工具..."
        python scripts\canoe_cicd_tool.py --workspace . --timeout 180
        
        echo "测试工具执行完成"
    
    - name: Upload test results
      if: always()
      uses: actions/upload-artifact@v3
      with:
        name: canoe-test-results-${{ github.run_id }}
        path: |
          results/
          logs/
        retention-days: 30
    
    - name: Generate test report
      if: always()
      uses: dorny/test-reporter@v1
      with:
        name: CANoe Test Results
        path: results/**/junit.xml
        reporter: jest-junit
    
    - name: Check for Demo limitations
      if: always()
      shell: cmd
      run: |
        echo "检查Demo版限制..."
        echo "注意: CANoe Demo版有以下限制:" >> $env:GITHUB_STEP_SUMMARY
        echo "1. 30分钟自动停止" >> $env:GITHUB_STEP_SUMMARY
        echo "2. 不能保存工程修改" >> $env:GITHUB_STEP_SUMMARY
        echo "3. 仅供学习使用" >> $env:GITHUB_STEP_SUMMARY
  
  # 作业3: 报告生成和通知
  report-and-notify:
    name: Generate Report and Notify
    needs: build-and-test
    if: always()
    runs-on: [self-hosted, canoe, windows]
    
    steps:
    - name: Download test results
      uses: actions/download-artifact@v3
      with:
        name: canoe-test-results-${{ github.run_id }}
        
    - name: Generate summary report
      shell: cmd
      run: |
        echo "生成测试摘要报告..."
        
        echo "# CANoe CI/CD测试报告" >> $env:GITHUB_STEP_SUMMARY
        echo "" >> $env:GITHUB_STEP_SUMMARY
        echo "**运行ID**: ${{ github.run_id }}" >> $env:GITHUB_STEP_SUMMARY
        echo "**触发事件**: ${{ github.event_name }}" >> $env:GITHUB_STEP_SUMMARY
        echo "**提交**: ${{ github.sha }}" >> $env:GITHUB_STEP_SUMMARY
        echo "**分支**: ${{ github.ref_name }}" >> $env:GITHUB_STEP_SUMMARY
        echo "" >> $env:GITHUB_STEP_SUMMARY
        
        echo "## 作业状态" >> $env:GITHUB_STEP_SUMMARY
        echo "- 代码检查: ${{ needs.code-quality.result }}" >> $env:GITHUB_STEP_SUMMARY
        echo "- 构建测试: ${{ needs.build-and-test.result }}" >> $env:GITHUB_STEP_SUMMARY
        echo "" >> $env:GITHUB_STEP_SUMMARY
        
        echo "## 生成的文件" >> $env:GITHUB_STEP_SUMMARY
        if exist results (
          for /d %%d in (results\*) do (
            echo "- **测试运行**: %%d" >> $env:GITHUB_STEP_SUMMARY
            for %%f in (%%d\*) do (
              echo "  - \`%%~nxf\`" >> $env:GITHUB_STEP_SUMMARY
            )
          )
        )
        
        echo "" >> $env:GITHUB_STEP_SUMMARY
        echo "---" >> $env:GITHUB_STEP_SUMMARY
        echo "*报告生成于 $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')*" >> $env:GITHUB_STEP_SUMMARY
    
    - name: Comment on PR
      if: github.event_name == 'pull_request' && failure()
      uses: actions/github-script@v6
      with:
        script: |
          const { owner, repo } = context.repo;
          const prNumber = context.issue.number;
          
          github.rest.issues.createComment({
            owner,
            repo,
            issue_number: prNumber,
            body: `## ❌ CANoe测试失败通知\n\n测试运行失败,请检查以下可能的问题:\n1. CAPL脚本语法错误\n2. 配置文件问题\n3. CANoe Demo版限制(30分钟超时)\n\n详细日志:[查看测试结果](${context.serverUrl}/${owner}/${repo}/actions/runs/${context.runId})`
          });
    
    - name: Success notification
      if: success()
      run: |
        echo "✅ 所有测试通过!" >> $env:GITHUB_STEP_SUMMARY
        echo "" >> $env:GITHUB_STEP_SUMMARY
        echo "CANoe CI/CD流水线执行成功,代码质量和功能测试均通过。" >> $env:GITHUB_STEP_SUMMARY

5.2 创建专用的Runner配置文件

# .github/runners/canoe-runner-config.yml
# CANoe专用Runner配置文件

runner:
  name: "canoe-test-server"
  labels: ["canoe", "windows", "self-hosted", "virtual-bus"]
  
  # 工作目录设置
  work: "C:\canoe-ci\work"
  
  # 环境要求
  requirements:
    - "CANoe Demo 17.0+"
    - "Windows 10/11 64-bit"
    - "Python 3.8+"
    - "8GB+ RAM"
    
  # 清理策略
  cleanup:
    - "每天凌晨3点清理工作目录"
    - "保留最近7天的测试结果"
    - "自动清理临时文件"
    
  # 监控配置
  monitoring:
    - "检查CANoe进程状态"
    - "监控磁盘空间"
    - "网络连通性检查"

# CANoe特定配置
canoe:
  # 安装路径
  install_path: "C:\Program Files\Vector\CANoe\Exec64"
  
  # 许可证信息(Demo版)
  license:
    type: "Demo"
    limitations:
      - "30分钟自动停止"
      - "不能保存修改"
      - "仅供学习使用"
  
  # 测试配置
  test_defaults:
    timeout: 300  # 5分钟
    channels:
      - type: "CAN"
        name: "Virtual_CAN"
        baudrate: 500
    
  # 报告设置
  reporting:
    formats: ["html", "xml", "junit"]
    retention_days: 30

# 安全配置
security:
  # 防火墙规则
  firewall:
    allow:
      - "github.com:443"
      - "api.github.com:443"
    
  # 访问控制
  access_control:
    allowed_users: ["ci-user"]
    admin_users: ["admin"]
  
  # 数据保护
  data_protection:
    clean_sensitive_data: true
    encrypt_logs: false

# 维护任务
maintenance:
  schedule:
    - task: "清理临时文件"
      cron: "0 3 * * *"
      command: "powershell -Command \"Remove-Item C:\\canoe-ci\\work\\* -Recurse -Force -ErrorAction SilentlyContinue\""
    
    - task: "检查更新"
      cron: "0 4 * * 0"  # 每周日凌晨4点
      command: "scripts\check_updates.bat"
    
    - task: "生成使用报告"
      cron: "0 5 * * *"
      command: "python scripts\generate_usage_report.py"

六、阶段四:完整学习项目示例(第1个月)

6.1 项目结构总览

canoe-learning-project/
├── .github/
│   ├── workflows/
│   │   ├── canoe-ci-cd.yml          # 主工作流
│   │   └── canoe-nightly.yml        # 夜间测试
│   └── runners/
│       └── canoe-runner-config.yml  # Runner配置
│
├── src/
│   ├── ECU_Simulator.can            # ECU模拟器
│   ├── Test_Monitor.can             # 测试监控
│   ├── Diagnostic_Server.can         # 诊断服务器
│   └── Test_Sequencer.can           # 测试序列器
│
├── config/
│   ├── demo_config.cfg              # Demo配置
│   ├── virtual_network.cfg          # 虚拟网络配置
│   └── test_setup.cfg               # 测试设置
│
├── scripts/
│   ├── run_canoe_test.bat           # 批处理测试脚本
│   ├── canoe_cicd_tool.py           # Python自动化工具
│   ├── setup-runner.ps1             # Runner安装脚本
│   └── check_environment.bat        # 环境检查
│
├── docs/
│   ├── PROJECT_STRUCTURE.md         # 项目结构
│   ├── CAPL_DOCUMENTATION.md        # CAPL文档
│   ├── CI_CD_GUIDE.md               # CI/CD指南
│   └── TROUBLESHOOTING.md           # 故障排除
│
├── tests/
│   ├── unit/
│   │   ├── test_capl_basic.can      # 基础测试
│   │   └── test_message_handling.can # 报文处理测试
│   └── integration/
│       └── test_full_communication.can # 完整通信测试
│
├── results/                         # 测试结果(自动生成)
│   └── .gitignore
│
├── build/                           # 构建输出(自动生成)
│   └── .gitignore
│
├── Makefile                         # 构建脚本
├── README.md                        # 项目说明
├── requirements.txt                 # Python依赖
└── .gitignore                       # Git忽略文件

6.2 README.md 项目说明

# CANoe Demo版学习项目

## 📖 项目概述

这个项目演示了如何在**零硬件投入**的情况下,使用CANoe Demo版学习:
1. **CAPL编程** - 车载网络自动化脚本语言
2. **GitHub CI/CD** - 现代自动化测试流程
3. **虚拟总线通信** - 无需真实硬件的测试环境

## 🚀 快速开始

### 前提条件
- Windows 10/11 64位系统
- [CANoe Demo版](https://www.vector.com/cn/zh/)(免费)
- Git
- Python 3.8+

### 安装步骤

1. **克隆项目**
   ```bash
   git clone https://github.com/your-username/canoe-learning.git
   cd canoe-learning
  1. 设置环境

    make setup
    
  2. 运行测试

    make test
    
  3. 模拟CI/CD流程

    make ci-test
    

🛠️ 项目结构

(项目结构如上所示)

📚 学习路径

第1周:CAPL基础

  • 学习基本的CAPL语法
  • 理解事件驱动编程
  • 编写简单的发送/接收脚本

第2周:GitHub Actions

  • 设置自托管Runner
  • 创建工作流文件
  • 理解触发条件

第3周:集成测试

  • 创建自动化测试脚本
  • 配置虚拟总线环境
  • 实现完整的测试流程

第4周:优化和扩展

  • 添加更多测试用例
  • 优化执行效率
  • 实现报告生成

🔧 技术栈

技术 用途 版本
CANoe Demo 虚拟总线仿真 17.0+
CAPL 自动化脚本 随CANoe
Python 自动化工具 3.8+
GitHub Actions CI/CD平台 最新
Windows 运行环境 10/11

📊 Demo版限制说明

⚠️ 重要提醒:CANoe Demo版有以下限制:

  1. 30分钟自动停止 - 所有测试应在30分钟内完成
  2. 不能保存修改 - 需要脚本自动生成配置
  3. 仅供学习 - 不可用于生产环境

🤝 贡献指南

  1. Fork本项目
  2. 创建特性分支 (git checkout -b feature/AmazingFeature)
  3. 提交更改 (git commit -m 'Add some AmazingFeature')
  4. 推送到分支 (git push origin feature/AmazingFeature)
  5. 打开Pull Request

📄 许可证

本项目采用MIT许可证 - 查看 LICENSE 文件了解详情

🙏 致谢

  • Vector Informatik GmbH - 提供CANoe Demo版
  • GitHub - 提供优秀的CI/CD平台
  • 所有贡献者和学习者

📞 支持

遇到问题?请:

  1. 查看 TROUBLESHOOTING.md
  2. 提交 Issue
  3. 参与讨论

Happy Learning! 🎉


## 七、故障排除指南

### 7.1 常见问题及解决方案

```markdown
# CANoe CI/CD故障排除指南

## 🚨 常见问题

### 问题1: CANoe未找到
**症状**: Runner报错"CANoe not found"
**解决方案**:
1. 确认CANoe已安装
   ```powershell
   Get-ChildItem "C:\Program Files\Vector\CANoe" -Recurse -Filter "CANoe64.exe"
  1. 添加CANoe到PATH
    [Environment]::SetEnvironmentVariable("PATH", "$env:PATH;C:\Program Files\Vector\CANoe\Exec64", "Machine")
    

问题2: Demo版超时

症状: 测试运行30分钟后自动停止
解决方案:

  1. 缩短测试时间到<25分钟
  2. 使用timeout-minutes: 25限制工作流
  3. 拆分长时间测试为多个短测试

问题3: GitHub Runner连接失败

症状: Runner无法连接到GitHub
解决方案:

  1. 检查防火墙设置
    New-NetFirewallRule -DisplayName "GitHub Runner" -Direction Outbound -Protocol TCP -RemotePort 443 -RemoteAddress "github.com" -Action Allow
    
  2. 验证代理设置
  3. 检查网络连通性

问题4: CAPL编译错误

症状: CANoe报告CAPL语法错误
解决方案:

  1. 使用本地CANoe验证脚本
  2. 检查特殊字符编码
  3. 确保使用正确的CANoe版本

问题5: 权限不足

症状: 脚本无法创建文件或目录
解决方案:

  1. 以管理员身份运行Runner服务
    .\svc stop
    .\svc uninstall
    .\svc install --user "NT AUTHORITY\SYSTEM"
    .\svc start
    
  2. 检查工作目录权限

🔧 调试技巧

启用详细日志

# 在workflow中启用调试
env:
  ACTIONS_STEP_DEBUG: true
  ACTIONS_RUNNER_DEBUG: true

本地测试Runner

# 以交互模式运行Runner
.\run.cmd

# 检查Runner状态
.\run.cmd --check

查看CANoe日志

# CANoe生成日志位置
$env:LOCALAPPDATA\Vector\CANoe\Logs

# 查看最近日志
Get-Content "$env:LOCALAPPDATA\Vector\CANoe\Logs\CANoe*.log" -Tail 50

📈 性能优化

减少测试时间

  1. 使用虚拟通道代替真实硬件
  2. 缩短测量时间
  3. 并行运行独立测试

资源管理

  1. 定期清理工作目录
  2. 监控磁盘空间
  3. 限制并发Runner数量

🆘 紧急恢复

Runner失去响应

  1. 重启Runner服务
    .\svc stop
    .\svc start
    
  2. 清理工作目录
  3. 重新注册Runner

测试环境损坏

  1. 重新安装CANoe Demo
  2. 重置Python环境
  3. 从干净状态重新开始

📞 获取帮助

如果以上方案无法解决问题:

  1. 检查文档 - 查看项目文档和注释
  2. 搜索Issue - 查看是否已有类似问题
  3. 提交新Issue - 提供详细的错误信息和日志
  4. 社区支持 - Vector官方论坛和GitHub社区

## 八、总结与展望

### 8.1 你已经学到了什么?

通过这个完整的学习项目,你已经掌握了:

1. **🎯 CAPL编程核心** - 事件驱动、报文处理、信号操作
2. **🔄 CI/CD自动化** - GitHub Actions工作流、自托管Runner
3. **🔧 工具链集成** - Python自动化、批处理脚本、Makefile
4. **📊 测试方法论** - 虚拟环境测试、自动化验证、报告生成
5. **🚀 项目管理** - 项目结构、文档编写、故障排除

### 8.2 下一步学习方向

```mermaid
graph LR
    A[当前: Demo版学习] --> B[下一步: 真实硬件]
    A --> C[下一步: 完整CANoe]
    A --> D[下一步: 高级CAPL]
    
    B --> E[连接VN1630A/VN5430]
    C --> F[购买正式License]
    D --> G[复杂测试用例]
    
    E --> H[真实ECU测试]
    F --> I[生产环境部署]
    G --> J[高级诊断功能]
    
    H --> K[完整HIL测试平台]
    I --> L[企业级CI/CD]
    J --> M[专家级自动化]

8.3 成本与收益分析

项目 成本 收益
CANoe Demo版 免费 完整的学习环境
GitHub账户 免费 强大的CI/CD平台
服务器PC 已有或旧电脑 自动化执行环境
学习时间 1-2个月 掌握核心技能
总计 几乎为零 职业竞争力大幅提升

8.4 最后的话

记住,最好的学习方式就是动手实践。这个项目为你提供了一个安全、免费、完整的学习环境:

  1. 不怕出错 - 虚拟环境不会损坏真实硬件
  2. 随时重试 - 一键即可重新开始
  3. 循序渐进 - 从简单到复杂的学习路径
  4. 实战导向 - 真实可用的CI/CD流程

现在就开始吧! 克隆项目,运行第一个测试,体验自动化测试的魅力。当你完成这个学习旅程后,你将不仅掌握了CANoe和CAPL,更掌握了现代汽车电子开发的自动化思维

祝你学习愉快,代码无Bug! 🚀


开始你的第一个提交:

git clone https://github.com/your-username/canoe-learning.git
cd canoe-learning
make setup
make test

看到绿色的"✅"了吗?恭喜,你的CANoe CI/CD之旅已经正式开始!

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐