CAN修复
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user