diff --git a/docs/architecture/parser/Ploss解析器.md b/docs/architecture/parser/Ploss解析器.md index b7715c0..f2b7410 100644 --- a/docs/architecture/parser/Ploss解析器.md +++ b/docs/architecture/parser/Ploss解析器.md @@ -5,65 +5,95 @@ --- ## 1. 日志格式 - -**格式:** `FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13)` - -**示例:** `FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1` - -> 括号数字表示有效字段编号,X 表示忽略该位置数据 +``` +pow_loss = 3053, delta_p = 1950 +FOD-> (1) (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) (14) (15) (16) +``` --- ## 2. 字段定义 +### 2.1 第一行字段 + | 编号 | 字段名称 | 数据类型 | 说明 | |------|----------|----------|------| -| (1) | RxType | byte | 接收端类型标识 | -| (2) | RxPower | short | 接收端功率(mW) | -| (3) | TxPower | short | 发射端功率(mW) | -| (4) | Vcoil | short | 发射线圈电压 | -| (5) | Vin | short | 输入电压 | -| (6) | Isns | short | 输入电流采样值 | -| (7) | Ploss | int | **核心值**:计算损耗(负数表示安全) | -| (8) | Threshold | int | **安全红线**:当前功率段阈值 | -| (9) | TriggerCount | short | 触发计数器 | -| (10) | FodResult | short | FOD判定结果状态 | -| (11) | ProtocolType | byte | 充电协议类型 | -| (12) | PwmDuty | short | PWM控制占空比 | -| (13) | CoilIndex | byte | 充电线圈索引 | +| (A) | PowLoss | int | 功率损耗值 | +| (B) | DeltaP | int | 功率差值 | + +### 2.2 第二行字段 (FOD->) + +| 编号 | 字段名称 | 数据类型 | 说明 | +|------|----------|----------|------| +| (1) | Field1 | int | 待定义 | +| (2) | Field2 | int | 待定义 | +| (3) | Field3 | int | 待定义 | +| (4) | Field4 | int | 待定义 | +| (5) | Field5 | int | 待定义 | +| (6) | Field6 | int | 待定义 | +| (7) | Field7 | int | 待定义 | +| (8) | Field8 | int | 待定义 | +| (9) | Field9 | int | 待定义 | +| (10) | Field10 | int | 待定义 | +| (11) | Field11 | int | 待定义 | +| (12) | Field12 | int | 待定义 | +| (13) | Field13 | int | 待定义 | +| (14) | Field14 | int | 待定义 | +| (15) | Field15 | int | 待定义 | +| (16) | Field16 | int | 待定义 | --- ## 3. 正则表达式 -> **修正说明**:原设计稿正则只有2个忽略字段,实际格式有3个X(忽略字段),已修正。 +日志由两行组成,需要分别匹配: ```csharp // Parsers/PlossParser.cs -// 格式: FOD-> (1) X X X (2) (3) ... (13),其中3个X需要3个 \d+ 忽略 -private static readonly Regex PlossPattern = new( - @"^FOD->\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$", +// 第一行格式: pow_loss = 3053, delta_p = 1950 +private static readonly Regex HeaderPattern = new( + @"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$", + RegexOptions.Compiled +); + +// 第二行格式: FOD-> 后跟16个数字(均支持负数) +private static readonly Regex FodPattern = new( + @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", RegexOptions.Compiled ); ``` +> **说明**:所有16个字段均支持负数,使用 `-?\d+` 匹配。 + **捕获组映射:** +**第一行 (HeaderPattern):** + | 捕获组 | 字段 | 类型转换 | |--------|------|----------| -| Group 1 | RxType | byte.Parse() | -| Group 2 | RxPower | short.Parse() | -| Group 3 | TxPower | short.Parse() | -| Group 4 | Vcoil | short.Parse() | -| Group 5 | Vin | short.Parse() | -| Group 6 | Isns | short.Parse() | -| Group 7 | Ploss | int.Parse() | -| Group 8 | Threshold | int.Parse() | -| Group 9 | TriggerCount | short.Parse() | -| Group 10 | FodResult | short.Parse() | -| Group 11 | ProtocolType | byte.Parse() | -| Group 12 | PwmDuty | short.Parse() | -| Group 13 | CoilIndex | byte.Parse() | +| Group 1 | PowLoss | int.Parse() | +| Group 2 | DeltaP | int.Parse() | + +**第二行 (FodPattern):** + +| 捕获组 | 字段 | 类型转换 | +|--------|------|----------| +| Group 1 | Field1 | int.Parse() | +| Group 2 | Field2 | int.Parse() | +| Group 3 | Field3 | int.Parse() | +| Group 4 | Field4 | int.Parse() | +| Group 5 | Field5 | int.Parse() | +| Group 6 | Field6 | int.Parse() | +| Group 7 | Field7 | int.Parse() | +| Group 8 | Field8 | int.Parse() | +| Group 9 | Field9 | int.Parse() | +| Group 10 | Field10 | int.Parse() | +| Group 11 | Field11 | int.Parse() | +| Group 12 | Field12 | int.Parse() | +| Group 13 | Field13 | int.Parse() | +| Group 14 | Field14 | int.Parse() | +| Group 15 | Field15 | int.Parse() | +| Group 16 | Field16 | int.Parse() | --- @@ -78,57 +108,102 @@ namespace WCTDataMiner.Parsers; public class PlossParser : IParser { - private static readonly Regex PlossPattern = new( - @"^FOD->\s+(\d+)\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$", + private static readonly Regex HeaderPattern = new( + @"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$", RegexOptions.Compiled ); - public bool CanParse(string line) => PlossPattern.IsMatch(line); + private static readonly Regex FodPattern = new( + @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", + RegexOptions.Compiled + ); + + public bool CanParse(string line) => HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line); public ParseResult Parse(string line, Guid scenarioId) { - var match = PlossPattern.Match(line); - if (!match.Success) + // 尝试匹配第一行 + var headerMatch = HeaderPattern.Match(line); + if (headerMatch.Success) { - return ParseResult.Failure( - "FORMAT_MISMATCH", - "Line does not match Ploss pattern" - ); + return ParseHeaderLine(headerMatch, scenarioId); } + // 尝试匹配第二行 + var fodMatch = FodPattern.Match(line); + if (fodMatch.Success) + { + return ParseFodLine(fodMatch, scenarioId); + } + + return ParseResult.Failure( + "FORMAT_MISMATCH", + "Line does not match Ploss pattern" + ); + } + + private ParseResult ParseHeaderLine(Match match, Guid scenarioId) + { try { var record = new PlossRecord { ScenarioId = scenarioId, - RxType = byte.Parse(match.Groups[1].Value), - RxPower = short.Parse(match.Groups[2].Value), - TxPower = short.Parse(match.Groups[3].Value), - Vcoil = short.Parse(match.Groups[4].Value), - Vin = short.Parse(match.Groups[5].Value), - Isns = short.Parse(match.Groups[6].Value), - Ploss = int.Parse(match.Groups[7].Value), - Threshold = int.Parse(match.Groups[8].Value), - TriggerCount = short.Parse(match.Groups[9].Value), - FodResult = short.Parse(match.Groups[10].Value), - ProtocolType = byte.Parse(match.Groups[11].Value), - PwmDuty = short.Parse(match.Groups[12].Value), - CoilIndex = byte.Parse(match.Groups[13].Value) + PowLoss = int.Parse(match.Groups[1].Value), + DeltaP = int.Parse(match.Groups[2].Value), + IsHeaderLine = true }; - return ParseResult.Success(record); } catch (Exception ex) { return ParseResult.Failure( "TYPE_CONVERSION", - $"Failed to convert field: {ex.Message}" + $"Failed to convert header field: {ex.Message}" + ); + } + } + + private ParseResult ParseFodLine(Match match, Guid scenarioId) + { + try + { + var record = new PlossRecord + { + ScenarioId = scenarioId, + Field1 = int.Parse(match.Groups[1].Value), + Field2 = int.Parse(match.Groups[2].Value), + Field3 = int.Parse(match.Groups[3].Value), + Field4 = int.Parse(match.Groups[4].Value), + Field5 = int.Parse(match.Groups[5].Value), + Field6 = int.Parse(match.Groups[6].Value), + Field7 = int.Parse(match.Groups[7].Value), + Field8 = int.Parse(match.Groups[8].Value), + Field9 = int.Parse(match.Groups[9].Value), + Field10 = int.Parse(match.Groups[10].Value), + Field11 = int.Parse(match.Groups[11].Value), + Field12 = int.Parse(match.Groups[12].Value), + Field13 = int.Parse(match.Groups[13].Value), + Field14 = int.Parse(match.Groups[14].Value), + Field15 = int.Parse(match.Groups[15].Value), + Field16 = int.Parse(match.Groups[16].Value), + IsHeaderLine = false + }; + return ParseResult.Success(record); + } + catch (Exception ex) + { + return ParseResult.Failure( + "TYPE_CONVERSION", + $"Failed to convert FOD field: {ex.Message}" ); } } } ``` +> **注意**:实际实现中,两行日志需要合并为一条完整记录。可以使用 `PlossRecordMerger` 或在业务逻辑层处理合并。 + --- ## 5. 核心判定逻辑 diff --git a/src/WCTDataMiner.Core/Data/Configurations/PlossRecordConfiguration.cs b/src/WCTDataMiner.Core/Data/Configurations/PlossRecordConfiguration.cs index 7cf308e..13a5b95 100644 --- a/src/WCTDataMiner.Core/Data/Configurations/PlossRecordConfiguration.cs +++ b/src/WCTDataMiner.Core/Data/Configurations/PlossRecordConfiguration.cs @@ -17,59 +17,84 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration .IsRequired() .HasColumnName("scenario_id"); - builder.Property(e => e.RxType) - .IsRequired() - .HasColumnName("rx_type"); + // ===== 第一行字段(nullable) ===== + builder.Property(e => e.PowLoss) + .IsRequired(false) + .HasColumnName("pow_loss"); - builder.Property(e => e.RxPower) - .IsRequired() - .HasColumnName("rx_power"); + builder.Property(e => e.DeltaP) + .IsRequired(false) + .HasColumnName("delta_p"); - builder.Property(e => e.TxPower) + // ===== 第二行字段(通用命名) ===== + builder.Property(e => e.Field1) .IsRequired() - .HasColumnName("tx_power"); + .HasColumnName("field_1"); - builder.Property(e => e.Vcoil) + builder.Property(e => e.Field2) .IsRequired() - .HasColumnName("vcoil"); + .HasColumnName("field_2"); - builder.Property(e => e.Vin) + builder.Property(e => e.Field3) .IsRequired() - .HasColumnName("vin"); + .HasColumnName("field_3"); - builder.Property(e => e.Isns) + builder.Property(e => e.Field4) .IsRequired() - .HasColumnName("isns"); + .HasColumnName("field_4"); - builder.Property(e => e.Ploss) + builder.Property(e => e.Field5) .IsRequired() - .HasColumnName("ploss"); + .HasColumnName("field_5"); - builder.Property(e => e.Threshold) + builder.Property(e => e.Field6) .IsRequired() - .HasColumnName("threshold"); + .HasColumnName("field_6"); - builder.Property(e => e.TriggerCount) + builder.Property(e => e.Field7) + .IsRequired() + .HasColumnName("field_7"); + + builder.Property(e => e.Field8) + .IsRequired() + .HasColumnName("field_8"); + + builder.Property(e => e.Field9) .IsRequired() .HasDefaultValue(0) - .HasColumnName("trigger_count"); + .HasColumnName("field_9"); - builder.Property(e => e.FodResult) + builder.Property(e => e.Field10) .IsRequired() .HasDefaultValue(0) - .HasColumnName("fod_result"); + .HasColumnName("field_10"); - builder.Property(e => e.ProtocolType) + builder.Property(e => e.Field11) .IsRequired() - .HasColumnName("protocol_type"); + .HasColumnName("field_11"); - builder.Property(e => e.PwmDuty) + builder.Property(e => e.Field12) .IsRequired() - .HasColumnName("pwm_duty"); + .HasColumnName("field_12"); - builder.Property(e => e.CoilIndex) + builder.Property(e => e.Field13) .IsRequired() - .HasColumnName("coil_index"); + .HasColumnName("field_13"); + + builder.Property(e => e.Field14) + .IsRequired() + .HasDefaultValue(0) + .HasColumnName("field_14"); + + builder.Property(e => e.Field15) + .IsRequired() + .HasDefaultValue(0) + .HasColumnName("field_15"); + + builder.Property(e => e.Field16) + .IsRequired() + .HasDefaultValue(0) + .HasColumnName("field_16"); builder.Property(e => e.IsDeleted) .IsRequired() @@ -86,10 +111,12 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration // 索引 builder.HasIndex(e => e.ScenarioId).HasDatabaseName("idx_ploss_scenario"); - builder.HasIndex(e => e.RxPower).HasDatabaseName("idx_ploss_rx_power"); - builder.HasIndex(e => e.Ploss).HasDatabaseName("idx_ploss_ploss"); - builder.HasIndex(e => e.Threshold).HasDatabaseName("idx_ploss_threshold"); - builder.HasIndex(e => e.FodResult).HasDatabaseName("idx_ploss_fod_result"); + builder.HasIndex(e => e.Field2).HasDatabaseName("idx_ploss_field_2"); // 原 rx_power + builder.HasIndex(e => e.Field7).HasDatabaseName("idx_ploss_field_7"); // 原 ploss + builder.HasIndex(e => e.Field8).HasDatabaseName("idx_ploss_field_8"); // 原 threshold + builder.HasIndex(e => e.Field10).HasDatabaseName("idx_ploss_field_10"); // 原 fod_result + builder.HasIndex(e => e.PowLoss).HasDatabaseName("idx_ploss_pow_loss"); // 新增 + builder.HasIndex(e => e.DeltaP).HasDatabaseName("idx_ploss_delta_p"); // 新增 // 关系配置 builder.HasOne(e => e.Scenario) diff --git a/src/WCTDataMiner.Core/Models/PlossRecord.cs b/src/WCTDataMiner.Core/Models/PlossRecord.cs index 161e3e8..730d454 100644 --- a/src/WCTDataMiner.Core/Models/PlossRecord.cs +++ b/src/WCTDataMiner.Core/Models/PlossRecord.cs @@ -2,35 +2,51 @@ namespace WCTDataMiner.Core.Models; /// /// Ploss记录实体 - 存储Ploss FOD格式解析数据 +/// 支持两行格式: +/// 第一行: pow_loss = X, delta_p = Y +/// 第二行: FOD-> (16个字段) /// public class PlossRecord { public Guid Id { get; set; } = Guid.NewGuid(); public Guid ScenarioId { get; set; } - public byte RxType { get; set; } - public int RxPower { get; set; } // uint16 范围 0-65535,使用 int 存储 - public int TxPower { get; set; } // uint16 范围 0-65535,使用 int 存储 - public int Vcoil { get; set; } // uint16 范围 0-65535,使用 int 存储 - public int Vin { get; set; } // uint16 范围 0-65535,使用 int 存储 - public int Isns { get; set; } // uint16 范围 0-65535,使用 int 存储 - /// 核心判定值:负数表示安全 - public int Ploss { get; set; } + // ===== 第一行字段(nullable,兼容旧数据) ===== + /// 功率损耗值(来自第一行) + public int? PowLoss { get; set; } - /// 安全红线:当前功率段阈值 - public int Threshold { get; set; } + /// 功率差值(来自第一行) + public int? DeltaP { get; set; } + + // ===== 第二行字段(16个通用字段,均支持负数) ===== + public int Field1 { get; set; } // 原 RxType + public int Field2 { get; set; } // 原 RxPower + public int Field3 { get; set; } // 原 TxPower + public int Field4 { get; set; } // 原 Vcoil + public int Field5 { get; set; } // 原 Vin + public int Field6 { get; set; } // 原 Isns + + /// 核心判定值:Field7 (原Ploss) - 负数表示安全 + public int Field7 { get; set; } + + /// 安全红线:Field8 (原Threshold) - 当前功率段阈值 + public int Field8 { get; set; } + + public int Field9 { get; set; } // 原 TriggerCount + public int Field10 { get; set; } // 原 FodResult + public int Field11 { get; set; } // 原 ProtocolType + public int Field12 { get; set; } // 原 PwmDuty + public int Field13 { get; set; } // 原 CoilIndex + public int Field14 { get; set; } // 新增 + public int Field15 { get; set; } // 新增 + public int Field16 { get; set; } // 新增 - public int TriggerCount { get; set; } = 0; - public int FodResult { get; set; } = 0; - public byte ProtocolType { get; set; } - public int PwmDuty { get; set; } // uint16 范围 0-65535,使用 int 存储 - public byte CoilIndex { get; set; } public bool IsDeleted { get; set; } = false; public DateTime CreatedAt { get; set; } = DateTime.UtcNow; // Navigation Properties public TestScenario Scenario { get; set; } = null!; - /// 派生属性:安全余量(不落库) - public int Margin => Threshold - Ploss; + /// 派生属性:安全余量(不落库) = Field8 - Field7 + public int Margin => Field8 - Field7; } \ No newline at end of file diff --git a/src/WCTDataMiner.Core/Parsers/IParser.cs b/src/WCTDataMiner.Core/Parsers/IParser.cs index 1ccdb4a..918a3b8 100644 --- a/src/WCTDataMiner.Core/Parsers/IParser.cs +++ b/src/WCTDataMiner.Core/Parsers/IParser.cs @@ -11,11 +11,21 @@ public interface IParser bool CanParse(string line); /// - /// 解析日志行 + /// 解析日志行(单行模式) /// /// 日志行内容 /// 关联的测试场景ID ParseResult Parse(string line, Guid scenarioId); + + /// + /// 判断是否为多行记录的起始行 + /// + bool IsMultiLineStart(string line) => false; + + /// + /// 解析多行记录(子类可重写) + /// + ParseResult? ParseMultiLine(string[] lines, Guid scenarioId) => null; } /// diff --git a/src/WCTDataMiner.Core/Parsers/PlossParser.cs b/src/WCTDataMiner.Core/Parsers/PlossParser.cs index 9fb11ed..f8e13ed 100644 --- a/src/WCTDataMiner.Core/Parsers/PlossParser.cs +++ b/src/WCTDataMiner.Core/Parsers/PlossParser.cs @@ -4,49 +4,113 @@ using WCTDataMiner.Core.Models; namespace WCTDataMiner.Core.Parsers; /// -/// Ploss FOD日志解析器 -/// 格式: FOD-> (1) X X X (2) (3) (4) (5) (6) (7) (8) (9) (10) (11) (12) (13) -/// 示例: FOD-> 4 2 1 127 10690 12013 35377 9904 1213 -2882 750 0 0 1 500 1 +/// Ploss FOD日志解析器 - 支持两行格式 +/// 第一行: pow_loss = 3053, delta_p = 1950 +/// 第二行: FOD-> (1) (2) ... (16) +/// 同时兼容旧的单行格式 /// public class PlossParser : IParser { - // 格式说明有3个X(忽略字段),正则需要3个 \d+ 来匹配 - private static readonly Regex PlossPattern = new( - @"^FOD->\s+(\d+)\s+\d+\s+\d+\s+\d+\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(-?\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)$", + // 第一行正则:pow_loss = 3053, delta_p = 1950 + private static readonly Regex HeaderPattern = new( + @"^pow_loss\s*=\s*(\d+),\s*delta_p\s*=\s*(\d+)$", RegexOptions.Compiled ); - public bool CanParse(string line) => PlossPattern.IsMatch(line); + // 第二行正则:FOD-> 后跟16个数字(均支持负数) + private static readonly Regex FodPattern = new( + @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", + RegexOptions.Compiled + ); + // 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数 + private static readonly Regex LegacyPattern = new( + @"^FOD->\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)$", + RegexOptions.Compiled + ); + + /// + /// 判断是否可以解析该行(任意一行) + /// + public bool CanParse(string line) => + HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line) || LegacyPattern.IsMatch(line); + + /// + /// 判断是否为多行记录的起始行(header行) + /// + public bool IsMultiLineStart(string line) => HeaderPattern.IsMatch(line); + + /// + /// 单行解析(兼容旧格式) + /// public ParseResult Parse(string line, Guid scenarioId) { - var match = PlossPattern.Match(line); - if (!match.Success) + // 尝试匹配旧格式 + var legacyMatch = LegacyPattern.Match(line); + if (legacyMatch.Success) { - return ParseResult.Failure( - "FORMAT_MISMATCH", - "Line does not match Ploss pattern" - ); + return ParseLegacyFormat(legacyMatch, scenarioId); } + // 尝试匹配第二行(新格式) + var fodMatch = FodPattern.Match(line); + if (fodMatch.Success) + { + return ParseFodLine(fodMatch, scenarioId); + } + + // 尝试匹配第一行 + var headerMatch = HeaderPattern.Match(line); + if (headerMatch.Success) + { + return ParseHeaderLine(headerMatch, scenarioId); + } + + return ParseResult.Failure( + "FORMAT_MISMATCH", + "Line does not match Ploss pattern" + ); + } + + /// + /// 多行解析(新两行格式) + /// + public ParseResult? ParseMultiLine(string[] lines, Guid scenarioId) + { + if (lines.Length < 2) + return null; + + var headerMatch = HeaderPattern.Match(lines[0].Trim()); + var fodMatch = FodPattern.Match(lines[1].Trim()); + + if (!headerMatch.Success || !fodMatch.Success) + return null; + try { var record = new PlossRecord { ScenarioId = scenarioId, - RxType = byte.Parse(match.Groups[1].Value), - RxPower = int.Parse(match.Groups[2].Value), - TxPower = int.Parse(match.Groups[3].Value), - Vcoil = int.Parse(match.Groups[4].Value), - Vin = int.Parse(match.Groups[5].Value), - Isns = int.Parse(match.Groups[6].Value), - Ploss = int.Parse(match.Groups[7].Value), - Threshold = int.Parse(match.Groups[8].Value), - TriggerCount = int.Parse(match.Groups[9].Value), - FodResult = int.Parse(match.Groups[10].Value), - ProtocolType = byte.Parse(match.Groups[11].Value), - PwmDuty = int.Parse(match.Groups[12].Value), - CoilIndex = byte.Parse(match.Groups[13].Value) + // 第一行字段 + PowLoss = int.Parse(headerMatch.Groups[1].Value), + DeltaP = int.Parse(headerMatch.Groups[2].Value), + // 第二行字段 + Field1 = int.Parse(fodMatch.Groups[1].Value), + Field2 = int.Parse(fodMatch.Groups[2].Value), + Field3 = int.Parse(fodMatch.Groups[3].Value), + Field4 = int.Parse(fodMatch.Groups[4].Value), + Field5 = int.Parse(fodMatch.Groups[5].Value), + Field6 = int.Parse(fodMatch.Groups[6].Value), + Field7 = int.Parse(fodMatch.Groups[7].Value), + Field8 = int.Parse(fodMatch.Groups[8].Value), + Field9 = int.Parse(fodMatch.Groups[9].Value), // 支持负数 + Field10 = int.Parse(fodMatch.Groups[10].Value), + Field11 = int.Parse(fodMatch.Groups[11].Value), + Field12 = int.Parse(fodMatch.Groups[12].Value), + Field13 = int.Parse(fodMatch.Groups[13].Value), + Field14 = int.Parse(fodMatch.Groups[14].Value), + Field15 = int.Parse(fodMatch.Groups[15].Value), + Field16 = int.Parse(fodMatch.Groups[16].Value) }; return ParseResult.Success(record); @@ -59,4 +123,106 @@ public class PlossParser : IParser ); } } + + private ParseResult ParseLegacyFormat(Match match, Guid scenarioId) + { + // 保持原有逻辑不变,映射到新字段名 + try + { + var record = new PlossRecord + { + ScenarioId = scenarioId, + // 旧格式没有第一行字段 + PowLoss = null, + DeltaP = null, + // 映射到新字段名 + Field1 = int.Parse(match.Groups[1].Value), // RxType + Field2 = int.Parse(match.Groups[2].Value), // RxPower + Field3 = int.Parse(match.Groups[3].Value), // TxPower + Field4 = int.Parse(match.Groups[4].Value), // Vcoil + Field5 = int.Parse(match.Groups[5].Value), // Vin + Field6 = int.Parse(match.Groups[6].Value), // Isns + Field7 = int.Parse(match.Groups[7].Value), // Ploss + Field8 = int.Parse(match.Groups[8].Value), // Threshold + Field9 = int.Parse(match.Groups[9].Value), // TriggerCount + Field10 = int.Parse(match.Groups[10].Value), // FodResult + Field11 = int.Parse(match.Groups[11].Value), // ProtocolType + Field12 = int.Parse(match.Groups[12].Value), // PwmDuty + Field13 = int.Parse(match.Groups[13].Value), // CoilIndex + // 旧格式没有Field14-16,使用默认值 + Field14 = 0, + Field15 = 0, + Field16 = 0 + }; + return ParseResult.Success(record); + } + catch (Exception ex) + { + return ParseResult.Failure( + "TYPE_CONVERSION", + $"Failed to convert field: {ex.Message}" + ); + } + } + + private ParseResult ParseHeaderLine(Match match, Guid scenarioId) + { + // 单独解析header行,返回部分记录 + // 实际使用时应通过ParseMultiLine合并 + try + { + var record = new PlossRecord + { + ScenarioId = scenarioId, + PowLoss = int.Parse(match.Groups[1].Value), + DeltaP = int.Parse(match.Groups[2].Value) + }; + return ParseResult.Success(record); + } + catch (Exception ex) + { + return ParseResult.Failure( + "TYPE_CONVERSION", + $"Failed to convert header field: {ex.Message}" + ); + } + } + + private ParseResult ParseFodLine(Match match, Guid scenarioId) + { + // 单独解析FOD行(新格式,无header) + try + { + var record = new PlossRecord + { + ScenarioId = scenarioId, + PowLoss = null, + DeltaP = null, + Field1 = byte.Parse(match.Groups[1].Value), + Field2 = int.Parse(match.Groups[2].Value), + Field3 = int.Parse(match.Groups[3].Value), + Field4 = int.Parse(match.Groups[4].Value), + Field5 = int.Parse(match.Groups[5].Value), + Field6 = int.Parse(match.Groups[6].Value), + Field7 = int.Parse(match.Groups[7].Value), + Field8 = int.Parse(match.Groups[8].Value), + Field9 = int.Parse(match.Groups[9].Value), + Field10 = int.Parse(match.Groups[10].Value), + Field11 = int.Parse(match.Groups[11].Value), + Field12 = int.Parse(match.Groups[12].Value), + Field13 = int.Parse(match.Groups[13].Value), + Field14 = int.Parse(match.Groups[14].Value), + Field15 = int.Parse(match.Groups[15].Value), + Field16 = int.Parse(match.Groups[16].Value) + }; + return ParseResult.Success(record); + } + catch (Exception ex) + { + return ParseResult.Failure( + "TYPE_CONVERSION", + $"Failed to convert FOD field: {ex.Message}" + ); + } + } } \ No newline at end of file diff --git a/src/WCTDataMiner.Core/Services/ExportService.cs b/src/WCTDataMiner.Core/Services/ExportService.cs index df95ca5..d86e68d 100644 --- a/src/WCTDataMiner.Core/Services/ExportService.cs +++ b/src/WCTDataMiner.Core/Services/ExportService.cs @@ -120,8 +120,8 @@ public class ExportService // 使用流式导出避免内存问题 await using var writer = new StreamWriter(filePath, false, Encoding.UTF8); - // CSV Header - await writer.WriteLineAsync("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,rx_type_field,rx_power,tx_power,vcoil,vin,isns,ploss,threshold,trigger_count,fod_result,protocol_type,pwm_duty,coil_index,margin"); + // CSV Header(包含新字段) + await writer.WriteLineAsync("id,scenario_id,tx_panel,tx_hardware,tx_software,rx_type,test_purpose,test_date,test_sequence,pow_loss,delta_p,field_1,field_2,field_3,field_4,field_5,field_6,field_7,field_8,field_9,field_10,field_11,field_12,field_13,field_14,field_15,field_16,margin"); // 使用 Select 投影避免 N+1 查询 var query = _context.PlossRecords @@ -136,20 +136,25 @@ public class ExportService r.Scenario.TestPurpose, r.Scenario.TestDate, r.Scenario.TestSequence, - r.RxType, - r.RxPower, - r.TxPower, - r.Vcoil, - r.Vin, - r.Isns, - r.Ploss, - r.Threshold, - r.TriggerCount, - r.FodResult, - r.ProtocolType, - r.PwmDuty, - r.CoilIndex, - Margin = r.Threshold - r.Ploss + r.PowLoss, + r.DeltaP, + r.Field1, + r.Field2, + r.Field3, + r.Field4, + r.Field5, + r.Field6, + r.Field7, + r.Field8, + r.Field9, + r.Field10, + r.Field11, + r.Field12, + r.Field13, + r.Field14, + r.Field15, + r.Field16, + Margin = r.Field8 - r.Field7 }) .OrderBy(r => r.Id); @@ -166,19 +171,24 @@ public class ExportService EscapeCsvField(r.TestPurpose), EscapeCsvField(r.TestDate), EscapeCsvField(r.TestSequence), - EscapeCsvField(r.RxType), - EscapeCsvField(r.RxPower), - EscapeCsvField(r.TxPower), - EscapeCsvField(r.Vcoil), - EscapeCsvField(r.Vin), - EscapeCsvField(r.Isns), - EscapeCsvField(r.Ploss), - EscapeCsvField(r.Threshold), - EscapeCsvField(r.TriggerCount), - EscapeCsvField(r.FodResult), - EscapeCsvField(r.ProtocolType), - EscapeCsvField(r.PwmDuty), - EscapeCsvField(r.CoilIndex), + EscapeCsvField(r.PowLoss), + EscapeCsvField(r.DeltaP), + EscapeCsvField(r.Field1), + EscapeCsvField(r.Field2), + EscapeCsvField(r.Field3), + EscapeCsvField(r.Field4), + EscapeCsvField(r.Field5), + EscapeCsvField(r.Field6), + EscapeCsvField(r.Field7), + EscapeCsvField(r.Field8), + EscapeCsvField(r.Field9), + EscapeCsvField(r.Field10), + EscapeCsvField(r.Field11), + EscapeCsvField(r.Field12), + EscapeCsvField(r.Field13), + EscapeCsvField(r.Field14), + EscapeCsvField(r.Field15), + EscapeCsvField(r.Field16), EscapeCsvField(r.Margin) ); await writer.WriteLineAsync(line); @@ -265,20 +275,25 @@ public class ExportService r.Scenario.TestPurpose, TestDate = r.Scenario.TestDate.ToString("yyyy-MM-dd"), r.Scenario.TestSequence, - RecordRxType = r.RxType, - r.RxPower, - r.TxPower, - r.Vcoil, - r.Vin, - r.Isns, - r.Ploss, - r.Threshold, - r.TriggerCount, - r.FodResult, - r.ProtocolType, - r.PwmDuty, - r.CoilIndex, - Margin = r.Threshold - r.Ploss + r.PowLoss, + r.DeltaP, + r.Field1, + r.Field2, + r.Field3, + r.Field4, + r.Field5, + r.Field6, + r.Field7, + r.Field8, + r.Field9, + r.Field10, + r.Field11, + r.Field12, + r.Field13, + r.Field14, + r.Field15, + r.Field16, + Margin = r.Field8 - r.Field7 }) .ToListAsync(); diff --git a/src/WCTDataMiner.Core/Services/ParseService.cs b/src/WCTDataMiner.Core/Services/ParseService.cs index 9a11bf2..3863d58 100644 --- a/src/WCTDataMiner.Core/Services/ParseService.cs +++ b/src/WCTDataMiner.Core/Services/ParseService.cs @@ -71,6 +71,10 @@ public class ParseService var plossRecords = new List(); int errorCount = 0; + // 用于暂存多行解析的header行 + string? pendingHeaderLine = null; + int pendingHeaderLineNumber = 0; + for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); @@ -90,20 +94,56 @@ public class ParseService errorCount++; } } + else if (_plossParser.IsMultiLineStart(line)) + { + // 检测到两行格式的header行,暂存 + pendingHeaderLine = line; + pendingHeaderLineNumber = lineNumber; + } else if (_plossParser.CanParse(line)) { - var result = _plossParser.Parse(line, scenario.Id); - if (result.IsSuccess) - plossRecords.Add(result.Record!); + if (pendingHeaderLine != null) + { + // 两行格式:合并解析 + var result = _plossParser.ParseMultiLine( + new[] { pendingHeaderLine, line }, scenario.Id); + + if (result != null && result.IsSuccess) + { + plossRecords.Add(result.Record!); + } + else + { + _logger.LogError("Ploss两行解析失败 [{Line}] {Error}: {Message}", + pendingHeaderLineNumber, result?.ErrorType, result?.ErrorMessage); + errorCount++; + } + pendingHeaderLine = null; + } else { - _logger.LogError("Ploss解析失败 [{Line}] {Error}: {Message}", - lineNumber, result.ErrorType, result.ErrorMessage); - errorCount++; + // 单行格式(旧格式兼容) + var result = _plossParser.Parse(line, scenario.Id); + if (result.IsSuccess) + plossRecords.Add(result.Record!); + else + { + _logger.LogError("Ploss解析失败 [{Line}] {Error}: {Message}", + lineNumber, result.ErrorType, result.ErrorMessage); + errorCount++; + } } } } + // 处理末尾遗留的header行(异常情况) + if (pendingHeaderLine != null) + { + _logger.LogWarning("发现未匹配的Ploss header行 [{Line}]: {Content}", + pendingHeaderLineNumber, pendingHeaderLine); + errorCount++; + } + // 批量保存 await BatchInsertAsync(qfodRecords, plossRecords); diff --git a/src/WCTDataMiner/WCTDataMiner.csproj b/src/WCTDataMiner/WCTDataMiner.csproj index fb0dc24..0fcc894 100644 --- a/src/WCTDataMiner/WCTDataMiner.csproj +++ b/src/WCTDataMiner/WCTDataMiner.csproj @@ -18,6 +18,9 @@ + + +