CAN修复

This commit is contained in:
“hsc”
2026-10-26 10:46:44 +08:00
parent e877dc9a90
commit 9cb139b208
12 changed files with 1462 additions and 167 deletions
+379
View File
@@ -0,0 +1,379 @@
using System.Globalization;
using System.Text;
namespace XCPCore
{
/// <summary>
/// A2L 中的测量量(MEASUREMENT)条目
/// </summary>
public sealed class A2lMeasurement
{
/// <summary>测量量名称</summary>
public string Name { get; init; } = string.Empty;
/// <summary>测量量描述(A2L 中的 LongIdentifier)</summary>
public string Description { get; init; } = string.Empty;
/// <summary>A2L 数据类型(UBYTE / UWORD / FLOAT32_IEEE 等)</summary>
public string DataType { get; init; } = string.Empty;
/// <summary>ECU 内存地址</summary>
public uint Address { get; init; }
/// <summary>占用字节数(由数据类型推导)</summary>
public byte SizeBytes { get; init; }
/// <summary>地址的 hex 文本</summary>
public string AddressText => $"0x{Address:X8}";
/// <summary>长度的显示文本</summary>
public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
/// <summary>列表 ToolTip 使用的完整描述</summary>
public string Detail => $"{Name} {AddressText} {DataType} {SizeText}\n{Description}".TrimEnd();
}
/// <summary>
/// A2L 中的标定量(CHARACTERISTIC)条目
/// </summary>
public sealed class A2lCharacteristic
{
/// <summary>标定量名称</summary>
public string Name { get; init; } = string.Empty;
/// <summary>标定量描述(A2L 中的 LongIdentifier)</summary>
public string Description { get; init; } = string.Empty;
/// <summary>标定量结构类型(VALUE / CURVE / MAP / VAL_BLK / ASCII 等)</summary>
public string CharacteristicType { get; init; } = string.Empty;
/// <summary>A2L 数据类型(由 RECORD_LAYOUT 的 FNC_VALUES 推导,未知时为空)</summary>
public string DataType { get; init; } = string.Empty;
/// <summary>ECU 内存地址</summary>
public uint Address { get; init; }
/// <summary>单元素字节数(未知时为 0)</summary>
public byte SizeBytes { get; init; }
/// <summary>地址的 hex 文本</summary>
public string AddressText => $"0x{Address:X8}";
/// <summary>长度的显示文本</summary>
public string SizeText => SizeBytes > 0 ? $"{SizeBytes}B" : "?";
/// <summary>列表 ToolTip 使用的完整描述</summary>
public string Detail => $"{Name} {AddressText} {CharacteristicType} {DataType} {SizeText}\n{Description}".TrimEnd();
}
/// <summary>
/// A2L(ASAM MCD-2 MC)描述文件解析结果
/// </summary>
public sealed class A2lDatabase
{
/// <summary>来源文件全路径</summary>
public string FilePath { get; set; } = string.Empty;
/// <summary>PROJECT 名称(A2L 中可能缺省)</summary>
public string ProjectName { get; set; } = string.Empty;
/// <summary>解析出的测量量列表</summary>
public List<A2lMeasurement> Measurements { get; } = new List<A2lMeasurement>();
/// <summary>解析出的标定量列表</summary>
public List<A2lCharacteristic> Characteristics { get; } = new List<A2lCharacteristic>();
/// <summary>解析结果摘要</summary>
public string Summary => $"MEASUREMENT {Measurements.Count} 项 · CHARACTERISTIC {Characteristics.Count} 项";
}
/// <summary>
/// 轻量 A2L 解析器:只提取 XCP 采集/标定所需的测量量、标定量及其地址与长度。
/// <para>
/// 支持块:MEASUREMENT、CHARACTERISTIC、RECORD_LAYOUT、PROJECT;其余块整体跳过。
/// 采用词法切分而非正则,能正确处理块内嵌套的 IF_DATA / AXIS_DESCR 等子块与 /* */ 注释。
/// </para>
/// </summary>
public static class A2lParser
{
private const char StringMark = '\u0001';
/// <summary>
/// 读取并解析 A2L 文件
/// </summary>
/// <param name="filePath">A2L 文件路径</param>
/// <returns>解析结果(无法识别的块被忽略,不抛异常)</returns>
public static A2lDatabase LoadFile(string filePath)
{
var db = Parse(ReadAllTextSmart(filePath));
db.FilePath = filePath;
return db;
}
/// <summary>
/// 解析 A2L 文本内容
/// </summary>
/// <param name="text">A2L 文件内容</param>
/// <returns>解析结果(无法识别的块被忽略,不抛异常)</returns>
public static A2lDatabase Parse(string text)
{
var db = new A2lDatabase();
if (string.IsNullOrWhiteSpace(text)) return db;
var recordLayouts = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var tokens = Tokenize(text);
for (int i = 0; i + 1 < tokens.Count; i++)
{
if (!IsKeyword(tokens[i], "/begin")) continue;
string block = tokens[i + 1].ToUpperInvariant();
int contentStart = i + 2;
int end = FindBlockEnd(tokens, contentStart, block);
if (end <= contentStart) { i = end; continue; }
switch (block)
{
case "PROJECT":
case "MODULE":
// 容器块:MEASUREMENT/CHARACTERISTIC 都嵌套在 MODULE 内,必须继续扫描而不能整块跳过
// /begin PROJECT <Name> <LongIdentifier>
if (block == "PROJECT" && contentStart < end)
{
db.ProjectName = tokens[contentStart];
}
continue;
case "RECORD_LAYOUT":
ParseRecordLayout(tokens, contentStart, end, recordLayouts);
break;
case "MEASUREMENT":
ParseMeasurement(tokens, contentStart, end, db);
break;
case "CHARACTERISTIC":
ParseCharacteristic(tokens, contentStart, end, recordLayouts, db);
break;
}
i = end;
}
return db;
}
/// <summary>解析 MEASUREMENT 块:/begin MEASUREMENT Name Desc DataType Conv Resolution Accuracy Lower Upper ... ECU_ADDRESS x</summary>
private static void ParseMeasurement(List<string> t, int start, int end, A2lDatabase db)
{
// Name + LongIdentifier + DataType + Conversion + Resolution + Accuracy + Lower + Upper = 8 个必备字段
if (end - start < 8) return;
string name = t[start];
if (name.Length == 0 || name[0] == '/') return;
uint address = 0;
for (int i = start + 8; i + 1 < end; i++)
{
if (IsKeyword(t[i], "ECU_ADDRESS"))
{
address = ParseHexUInt32(t[i + 1]);
break;
}
}
if (address == 0) return; // 没有地址的测量量无法用于 DAQ
string dataType = t[start + 2].ToUpperInvariant();
db.Measurements.Add(new A2lMeasurement
{
Name = name,
Description = Unquote(t[start + 1]),
DataType = dataType,
Address = address,
SizeBytes = GetDataTypeSize(dataType)
});
}
/// <summary>解析 CHARACTERISTIC 块:/begin CHARACTERISTIC Name Desc Type Address Deposit MaxDiff Conversion Lower Upper ...</summary>
private static void ParseCharacteristic(List<string> t, int start, int end,
Dictionary<string, string> recordLayouts, A2lDatabase db)
{
// Name + LongIdentifier + Type + Address + Deposit + MaxDiff + Conversion + Lower + Upper = 9 个必备字段
if (end - start < 9) return;
string name = t[start];
if (name.Length == 0 || name[0] == '/') return;
uint address = ParseHexUInt32(t[start + 3]);
if (address == 0) return;
string type = t[start + 2].ToUpperInvariant();
string deposit = t[start + 4];
string dataType = recordLayouts.TryGetValue(deposit, out var layoutType) ? layoutType : string.Empty;
db.Characteristics.Add(new A2lCharacteristic
{
Name = name,
Description = Unquote(t[start + 1]),
CharacteristicType = type,
DataType = dataType,
Address = address,
SizeBytes = GetCharacteristicSize(type, dataType)
});
}
/// <summary>解析 RECORD_LAYOUT 块,记录 FNC_VALUES 的元素数据类型,供 CHARACTERISTIC 推导长度</summary>
private static void ParseRecordLayout(List<string> t, int start, int end,
Dictionary<string, string> recordLayouts)
{
if (start >= end) return;
string layoutName = t[start];
for (int i = start + 1; i + 3 < end; i++)
{
if (IsKeyword(t[i], "FNC_VALUES"))
{
// FNC_VALUES <position> <datatype> <indexMode> <addressType>
recordLayouts[layoutName] = t[i + 2].ToUpperInvariant();
return;
}
}
}
/// <summary>标定量长度:VALUE 取元素大小;CURVE/MAP/VAL_BLK 的维度信息在 AXIS_DESCR 中,这里按单元素估算</summary>
private static byte GetCharacteristicSize(string characteristicType, string dataType)
{
if (characteristicType == "ASCII") return 1;
return GetDataTypeSize(dataType);
}
/// <summary>A2L 数据类型 → 字节数(未知返回 0)</summary>
private static byte GetDataTypeSize(string dataType)
{
switch (dataType)
{
case "UBYTE":
case "SBYTE":
case "BYTE":
return 1;
case "UWORD":
case "SWORD":
case "WORD":
return 2;
case "ULONG":
case "SLONG":
case "LONG":
return 4;
case "A_UINT64":
case "A_INT64":
return 8;
case "FLOAT16_IEEE":
case "FLOAT16":
return 2;
case "FLOAT32_IEEE":
case "FLOAT32":
return 4;
case "FLOAT64_IEEE":
case "FLOAT64":
return 8;
default:
return 0;
}
}
/// <summary>找到与 /begin 匹配的 /end 位置(支持同名块嵌套)</summary>
private static int FindBlockEnd(List<string> tokens, int start, string block)
{
int depth = 1;
for (int i = start; i + 1 < tokens.Count; i++)
{
if (!IsKeyword(tokens[i], "/begin") && !IsKeyword(tokens[i], "/end")) continue;
if (!string.Equals(tokens[i + 1], block, StringComparison.OrdinalIgnoreCase)) continue;
if (IsKeyword(tokens[i], "/begin"))
{
depth++;
}
else if (--depth == 0)
{
return i;
}
}
return tokens.Count;
}
/// <summary>词法切分:跳过 /* */ 与 // 注释,字符串字面量整体保留(首字符加标记区分)</summary>
private static List<string> Tokenize(string text)
{
var tokens = new List<string>(text.Length / 8);
int i = 0, n = text.Length;
while (i < n)
{
char c = text[i];
if (char.IsWhiteSpace(c)) { i++; continue; }
if (c == '/' && i + 1 < n && text[i + 1] == '*')
{
int commentEnd = text.IndexOf("*/", i + 2, StringComparison.Ordinal);
i = commentEnd < 0 ? n : commentEnd + 2;
continue;
}
if (c == '/' && i + 1 < n && text[i + 1] == '/')
{
int lineEnd = text.IndexOf('\n', i + 2);
i = lineEnd < 0 ? n : lineEnd + 1;
continue;
}
if (c == '"')
{
var sb = new StringBuilder();
i++;
while (i < n)
{
if (text[i] == '\\' && i + 1 < n) { sb.Append(text[i + 1]); i += 2; continue; }
if (text[i] == '"') { i++; break; }
sb.Append(text[i]);
i++;
}
tokens.Add(StringMark + sb.ToString());
continue;
}
int tokenStart = i;
while (i < n && !char.IsWhiteSpace(text[i]) && text[i] != '"'
&& !(text[i] == '/' && i + 1 < n && (text[i + 1] == '*' || text[i + 1] == '/')))
{
i++;
}
if (i > tokenStart) tokens.Add(text.Substring(tokenStart, i - tokenStart));
}
return tokens;
}
private static bool IsKeyword(string token, string keyword)
=> string.Equals(token, keyword, StringComparison.OrdinalIgnoreCase);
private static bool IsString(string token) => token.Length > 0 && token[0] == StringMark;
private static string Unquote(string token)
=> IsString(token) ? token[1..].Trim() : string.Empty;
/// <summary>A2L 中的地址一律按 hex 解析(允许 0x 前缀)</summary>
private static uint ParseHexUInt32(string token)
{
if (string.IsNullOrEmpty(token)) return 0;
var text = token.Trim();
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) text = text[2..];
return uint.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var value) ? value : 0;
}
/// <summary>按 BOM / UTF-8 严格校验读取文本,非 UTF-8(GBK 等)退化为 Latin1,保证 ASCII 关键字可解析</summary>
private static string ReadAllTextSmart(string filePath)
{
var bytes = File.ReadAllBytes(filePath);
if (bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF)
return Encoding.UTF8.GetString(bytes, 3, bytes.Length - 3);
if (bytes.Length >= 2 && bytes[0] == 0xFF && bytes[1] == 0xFE)
return Encoding.Unicode.GetString(bytes, 2, bytes.Length - 2);
if (bytes.Length >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF)
return Encoding.BigEndianUnicode.GetString(bytes, 2, bytes.Length - 2);
try
{
return new UTF8Encoding(false, true).GetString(bytes);
}
catch (DecoderFallbackException)
{
return Encoding.Latin1.GetString(bytes);
}
}
}
}