feat(parser): 支持 Ploss Field1 十六进制解析
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
using System.Globalization;
|
||||
using System.Text.RegularExpressions;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Domain.Local;
|
||||
|
||||
@@ -17,15 +18,18 @@ public class PlossParser : IParser<PlossRecord>
|
||||
RegexOptions.Compiled
|
||||
);
|
||||
|
||||
// 第二行正则:FOD-> 后跟16个数字(均支持负数)
|
||||
// 第二行正则:FOD-> 后跟16个字段
|
||||
// Field1 支持十六进制格式(0X/0x 前缀,如 0C、0xFF)或普通十进制;
|
||||
// 其余字段为十进制整数(均支持负数)。
|
||||
// Field1 用捕获组是为了方便从 Match 取值(解析逻辑见 ParseFieldValue)。
|
||||
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+)$",
|
||||
@"^FOD->\s+(0[0-9A-Fa-f]+|-?\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+)\s+(-?\d+)$",
|
||||
RegexOptions.Compiled
|
||||
);
|
||||
|
||||
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数
|
||||
// 旧格式正则(兼容):前3个字段被忽略,所有捕获组支持负数;Field1 兼容十六进制
|
||||
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+)$",
|
||||
@"^FOD->\s+(0[0-9A-Fa-f]+|-?\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
|
||||
);
|
||||
|
||||
@@ -103,7 +107,7 @@ public class PlossParser : IParser<PlossRecord>
|
||||
PowLoss = int.Parse(headerMatch.Groups[1].Value),
|
||||
DeltaP = int.Parse(headerMatch.Groups[2].Value),
|
||||
// 第二行字段
|
||||
Field1 = int.Parse(fodMatch.Groups[1].Value),
|
||||
Field1 = ParseFieldValue(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),
|
||||
@@ -144,7 +148,7 @@ public class PlossParser : IParser<PlossRecord>
|
||||
PowLoss = null,
|
||||
DeltaP = null,
|
||||
// 映射到新字段名
|
||||
Field1 = int.Parse(match.Groups[1].Value),
|
||||
Field1 = ParseFieldValue(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), // RX power
|
||||
@@ -206,7 +210,7 @@ public class PlossParser : IParser<PlossRecord>
|
||||
ScenarioId = scenarioId,
|
||||
PowLoss = null,
|
||||
DeltaP = null,
|
||||
Field1 = byte.Parse(match.Groups[1].Value),
|
||||
Field1 = ParseFieldValue(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),
|
||||
@@ -233,4 +237,13 @@ public class PlossParser : IParser<PlossRecord>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 Field1 的值,支持十六进制格式(0C、0xFF)和普通十进制。
|
||||
/// 含 a-f/A-F 字符时按十六进制解析,否则按十进制解析。
|
||||
/// </summary>
|
||||
private static int ParseFieldValue(string value) =>
|
||||
value.AsSpan().IndexOfAny("abcdefABCDEF".AsSpan()) >= 0
|
||||
? int.Parse(value, NumberStyles.HexNumber)
|
||||
: int.Parse(value);
|
||||
}
|
||||
@@ -87,6 +87,26 @@ public class AnalyzeCommandTests : CommandTestBase
|
||||
Assert.That(stdout, Does.Contain("Please specify --pure and --foreign for Qfod calibration"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Analyze_Ploss_LegacyFile_HexField1_DecodedCorrectly()
|
||||
{
|
||||
var dir = NewDir();
|
||||
// 旧单行格式 Field1=0C (十六进制) 应被解析为十进制 12
|
||||
var file = TestFixtures.WriteLegacyPlossLog(
|
||||
dir,
|
||||
"singleMold-v1.0-hex2_1-iPhone15-ploss_legacy-20260710-1.log",
|
||||
new (int, int, int)[] { (500, 3000, 0) },
|
||||
field1Hex: "0C");
|
||||
|
||||
var (code, stdout, _) = await Analyze().RunCaptureAsync("--type", "ploss", "--file", file);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Total: 1 (TwoLine: 0, Legacy: 1), FOD: 0, Errors: 0"));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Analyze_Qfod_ValidFiles_ReportsThreshold()
|
||||
{
|
||||
|
||||
@@ -76,6 +76,30 @@ public class ParseCommandTests : CommandTestBase
|
||||
Assert.That(await Host.LocalDb.PlossRecords.CountAsync(), Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[TestCase("0C", 12)] // 上位 nibble = 0,下位 nibble = C
|
||||
[TestCase("0xFF", 255)] // 标准 0x 前缀
|
||||
[TestCase("0xff", 255)] // 小写 hex
|
||||
[TestCase("0A", 10)]
|
||||
[TestCase("0", 0)] // 纯 0:应识别为十进制 0
|
||||
[TestCase("10", 10)] // 纯十进制:应识别为 10(不是 0x10 = 16)
|
||||
public async Task Parse_File_PlossLog_HexField1_DecodedCorrectly(string field1Raw, int expected)
|
||||
{
|
||||
var dir = NewDir();
|
||||
var file = TestFixtures.WritePlossLog(dir, TestFixtures.PlossFileName,
|
||||
new TestFixtures.PlossTwoLineRow(Field1AsHex: field1Raw));
|
||||
|
||||
var (code, stdout, _) = await Parse().RunCaptureAsync("--file", file);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(code, Is.EqualTo(0));
|
||||
Assert.That(stdout, Does.Contain("Ploss: 1"));
|
||||
});
|
||||
|
||||
var record = await Host.LocalDb.PlossRecords.SingleAsync();
|
||||
Assert.That(record.Field1, Is.EqualTo(expected));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Parse_File_WithForceOption_Accepted()
|
||||
{
|
||||
|
||||
@@ -13,10 +13,15 @@ public static class TestFixtures
|
||||
/// 两行 Ploss 记录(header + FOD 16 字段)。
|
||||
/// 默认值让恒等式成立(Field5 - Field4 - PowLoss - ploss == DeltaP)且余量 > 2000。
|
||||
/// </summary>
|
||||
/// <param name="Field1AsHex">
|
||||
/// 当不为 null 时,覆写 Field1,按十六进制字面量写入日志(如 "0C" = 十进制 12),
|
||||
/// 用于验证解析器对 Field1 十六进制格式的支持。
|
||||
/// </param>
|
||||
public sealed record PlossTwoLineRow(
|
||||
int PowLoss = 2200,
|
||||
int DeltaP = 2500,
|
||||
int Field1 = 1,
|
||||
string? Field1AsHex = null,
|
||||
int Field2 = 0,
|
||||
int Field3 = 0,
|
||||
int Field4 = 800, // RX power
|
||||
@@ -41,8 +46,9 @@ public static class TestFixtures
|
||||
foreach (var r in rows)
|
||||
{
|
||||
lines.Add($"pow_loss = {r.PowLoss}, delta_p = {r.DeltaP}");
|
||||
var field1 = r.Field1AsHex ?? r.Field1.ToString();
|
||||
lines.Add(
|
||||
$"FOD-> {r.Field1} {r.Field2} {r.Field3} {r.Field4} {r.Field5} {r.Field6} " +
|
||||
$"FOD-> {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}");
|
||||
}
|
||||
@@ -51,11 +57,18 @@ public static class TestFixtures
|
||||
}
|
||||
|
||||
/// <summary>写旧单行格式 Ploss 日志(14 字段),仅指定 ploss/threshold/FOD 三个关键字段。</summary>
|
||||
public static string WriteLegacyPlossLog(string dir, string fileName, params (int Ploss, int Threshold, int Fod)[] rows)
|
||||
/// <param name="rows">每行 FOD 记录关键字段(ploss/threshold/FOD)</param>
|
||||
/// <param name="field1Hex">若非 null 则覆盖首字段为十六进制字面量(否则固定为 "1")。</param>
|
||||
public static string WriteLegacyPlossLog(
|
||||
string dir,
|
||||
string fileName,
|
||||
(int Ploss, int Threshold, int Fod)[] rows,
|
||||
string? field1Hex = null)
|
||||
{
|
||||
var path = Path.Combine(dir, fileName);
|
||||
var head = field1Hex ?? "1";
|
||||
var lines = rows
|
||||
.Select(r => $"FOD-> 1 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0")
|
||||
.Select(r => $"FOD-> {head} 0 0 0 0 0 0 0 {r.Ploss} {r.Threshold} 0 {r.Fod} 0 0")
|
||||
.ToArray();
|
||||
File.WriteAllLines(path, lines);
|
||||
return path;
|
||||
|
||||
Reference in New Issue
Block a user