bugfix: 修复已知bug
This commit is contained in:
@@ -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<PlossRecord>
|
||||
{
|
||||
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<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = PlossPattern.Match(line);
|
||||
if (!match.Success)
|
||||
// 尝试匹配第一行
|
||||
var headerMatch = HeaderPattern.Match(line);
|
||||
if (headerMatch.Success)
|
||||
{
|
||||
return ParseResult<PlossRecord>.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<PlossRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Ploss pattern"
|
||||
);
|
||||
}
|
||||
|
||||
private ParseResult<PlossRecord> 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<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
$"Failed to convert header field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ParseResult<PlossRecord> 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<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert FOD field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **注意**:实际实现中,两行日志需要合并为一条完整记录。可以使用 `PlossRecordMerger` 或在业务逻辑层处理合并。
|
||||
|
||||
---
|
||||
|
||||
## 5. 核心判定逻辑
|
||||
|
||||
@@ -17,59 +17,84 @@ public class PlossRecordConfiguration : IEntityTypeConfiguration<PlossRecord>
|
||||
.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<PlossRecord>
|
||||
|
||||
// 索引
|
||||
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)
|
||||
|
||||
@@ -2,35 +2,51 @@ namespace WCTDataMiner.Core.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss记录实体 - 存储Ploss FOD格式解析数据
|
||||
/// 支持两行格式:
|
||||
/// 第一行: pow_loss = X, delta_p = Y
|
||||
/// 第二行: FOD-> (16个字段)
|
||||
/// </summary>
|
||||
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 存储
|
||||
|
||||
/// <summary>核心判定值:负数表示安全</summary>
|
||||
public int Ploss { get; set; }
|
||||
// ===== 第一行字段(nullable,兼容旧数据) =====
|
||||
/// <summary>功率损耗值(来自第一行)</summary>
|
||||
public int? PowLoss { get; set; }
|
||||
|
||||
/// <summary>安全红线:当前功率段阈值</summary>
|
||||
public int Threshold { get; set; }
|
||||
/// <summary>功率差值(来自第一行)</summary>
|
||||
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
|
||||
|
||||
/// <summary>核心判定值:Field7 (原Ploss) - 负数表示安全</summary>
|
||||
public int Field7 { get; set; }
|
||||
|
||||
/// <summary>安全红线:Field8 (原Threshold) - 当前功率段阈值</summary>
|
||||
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!;
|
||||
|
||||
/// <summary>派生属性:安全余量(不落库)</summary>
|
||||
public int Margin => Threshold - Ploss;
|
||||
/// <summary>派生属性:安全余量(不落库) = Field8 - Field7</summary>
|
||||
public int Margin => Field8 - Field7;
|
||||
}
|
||||
@@ -11,11 +11,21 @@ public interface IParser<TRecord>
|
||||
bool CanParse(string line);
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志行
|
||||
/// 解析日志行(单行模式)
|
||||
/// </summary>
|
||||
/// <param name="line">日志行内容</param>
|
||||
/// <param name="scenarioId">关联的测试场景ID</param>
|
||||
ParseResult<TRecord> Parse(string line, Guid scenarioId);
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为多行记录的起始行
|
||||
/// </summary>
|
||||
bool IsMultiLineStart(string line) => false;
|
||||
|
||||
/// <summary>
|
||||
/// 解析多行记录(子类可重写)
|
||||
/// </summary>
|
||||
ParseResult<TRecord>? ParseMultiLine(string[] lines, Guid scenarioId) => null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -4,49 +4,113 @@ using WCTDataMiner.Core.Models;
|
||||
namespace WCTDataMiner.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
/// 同时兼容旧的单行格式
|
||||
/// </summary>
|
||||
public class PlossParser : IParser<PlossRecord>
|
||||
{
|
||||
// 格式说明有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
|
||||
);
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以解析该行(任意一行)
|
||||
/// </summary>
|
||||
public bool CanParse(string line) =>
|
||||
HeaderPattern.IsMatch(line) || FodPattern.IsMatch(line) || LegacyPattern.IsMatch(line);
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为多行记录的起始行(header行)
|
||||
/// </summary>
|
||||
public bool IsMultiLineStart(string line) => HeaderPattern.IsMatch(line);
|
||||
|
||||
/// <summary>
|
||||
/// 单行解析(兼容旧格式)
|
||||
/// </summary>
|
||||
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = PlossPattern.Match(line);
|
||||
if (!match.Success)
|
||||
// 尝试匹配旧格式
|
||||
var legacyMatch = LegacyPattern.Match(line);
|
||||
if (legacyMatch.Success)
|
||||
{
|
||||
return ParseResult<PlossRecord>.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<PlossRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Ploss pattern"
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 多行解析(新两行格式)
|
||||
/// </summary>
|
||||
public ParseResult<PlossRecord>? 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<PlossRecord>.Success(record);
|
||||
@@ -59,4 +123,106 @@ public class PlossParser : IParser<PlossRecord>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ParseResult<PlossRecord> 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<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ParseResult<PlossRecord> 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<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert header field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private ParseResult<PlossRecord> 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<PlossRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert FOD field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -71,6 +71,10 @@ public class ParseService
|
||||
var plossRecords = new List<PlossRecord>();
|
||||
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);
|
||||
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Hosting" Version="8.0.0" />
|
||||
|
||||
<!-- EF Core Tools -->
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.0" />
|
||||
|
||||
<!-- Logging -->
|
||||
<PackageReference Include="Serilog" Version="4.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Hosting" Version="8.0.0" />
|
||||
|
||||
Reference in New Issue
Block a user