refactor(project): 迁移项目命名并移除WPF客户端
This commit is contained in:
69
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/FileNameParser.cs
Normal file
69
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/FileNameParser.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Globalization;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// 文件名解析器 - 从文件名提取场景信息
|
||||
/// 格式: TX面板-TX硬件-TX软件-RX类型-测试目的-年月日-测试次数.log
|
||||
/// 分隔符: 使用 '-' 作为字段分隔符,字段内部可包含 '_' 或 '.' 等符号
|
||||
/// 示例: singleMold-v1.0-hex2_1-iPhone15-compatibility-20260702-1.log
|
||||
/// </summary>
|
||||
public class FileNameParser
|
||||
{
|
||||
private static readonly char[] Separators = { '-' };
|
||||
|
||||
/// <summary>
|
||||
/// 解析日志文件名,提取场景信息
|
||||
/// </summary>
|
||||
/// <param name="fileName">文件名(不含路径)</param>
|
||||
/// <returns>场景信息,若格式不匹配返回 null</returns>
|
||||
public ScenarioInfo? Parse(string fileName)
|
||||
{
|
||||
// 移除 .log 扩展名
|
||||
var nameWithoutExt = Path.GetFileNameWithoutExtension(fileName);
|
||||
if (string.IsNullOrEmpty(nameWithoutExt))
|
||||
return null;
|
||||
|
||||
var parts = nameWithoutExt.Split(Separators);
|
||||
if (parts.Length < 7)
|
||||
return null;
|
||||
|
||||
// 解析日期
|
||||
if (!TryParseDate(parts[5], out var testDate))
|
||||
return null;
|
||||
|
||||
// 解析测试序号
|
||||
if (!int.TryParse(parts[6], out var testSequence))
|
||||
testSequence = 1;
|
||||
|
||||
return new ScenarioInfo(
|
||||
TxPanel: parts[0],
|
||||
TxHardware: parts[1],
|
||||
TxSoftware: parts[2],
|
||||
RxType: parts[3],
|
||||
TestPurpose: parts.Length > 4 ? parts[4] : null,
|
||||
TestDate: testDate,
|
||||
TestSequence: testSequence
|
||||
);
|
||||
}
|
||||
|
||||
private static bool TryParseDate(string dateStr, out DateOnly date)
|
||||
{
|
||||
// 支持多种日期格式
|
||||
var formats = new[] { "yyyyMMdd", "yyyy-MM-dd", "yyMMdd" };
|
||||
return DateOnly.TryParseExact(dateStr, formats, CultureInfo.InvariantCulture, DateTimeStyles.None, out date);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 场景信息 - 从文件名解析出的测试场景数据
|
||||
/// </summary>
|
||||
public record ScenarioInfo(
|
||||
string TxPanel,
|
||||
string TxHardware,
|
||||
string TxSoftware,
|
||||
string RxType,
|
||||
string? TestPurpose,
|
||||
DateOnly TestDate,
|
||||
int TestSequence
|
||||
);
|
||||
46
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/IParser.cs
Normal file
46
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/IParser.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// 通用解析器接口
|
||||
/// </summary>
|
||||
public interface IParser<TRecord>
|
||||
{
|
||||
/// <summary>
|
||||
/// 判断是否可以解析该行
|
||||
/// </summary>
|
||||
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>
|
||||
/// 解析结果
|
||||
/// </summary>
|
||||
public record ParseResult<TRecord>
|
||||
{
|
||||
public bool IsSuccess { get; init; }
|
||||
public TRecord? Record { get; init; }
|
||||
public string? ErrorType { get; init; }
|
||||
public string? ErrorMessage { get; init; }
|
||||
|
||||
public static ParseResult<TRecord> Success(TRecord record) =>
|
||||
new() { IsSuccess = true, Record = record };
|
||||
|
||||
public static ParseResult<TRecord> Failure(string errorType, string errorMessage) =>
|
||||
new() { IsSuccess = false, ErrorType = errorType, ErrorMessage = errorMessage };
|
||||
}
|
||||
236
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/PlossParser.cs
Normal file
236
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/PlossParser.cs
Normal file
@@ -0,0 +1,236 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Ploss FOD日志解析器 - 支持两行格式
|
||||
/// 第一行: pow_loss = 3053, delta_p = 1950
|
||||
/// 第二行: FOD-> (1) (2) ... (16)
|
||||
/// 同时兼容旧的单行格式
|
||||
/// </summary>
|
||||
public class PlossParser : IParser<PlossRecord>
|
||||
{
|
||||
// 第一行正则: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
|
||||
);
|
||||
|
||||
// 旧格式正则(兼容):前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 enum MatchType { None, Header, Fod, Legacy }
|
||||
|
||||
/// <summary>
|
||||
/// 单次匹配结果(避免重复正则匹配)
|
||||
/// </summary>
|
||||
public (MatchType Type, Match Match) TryMatch(string line)
|
||||
{
|
||||
var headerMatch = HeaderPattern.Match(line);
|
||||
if (headerMatch.Success)
|
||||
return (MatchType.Header, headerMatch);
|
||||
|
||||
var fodMatch = FodPattern.Match(line);
|
||||
if (fodMatch.Success)
|
||||
return (MatchType.Fod, fodMatch);
|
||||
|
||||
var legacyMatch = LegacyPattern.Match(line);
|
||||
if (legacyMatch.Success)
|
||||
return (MatchType.Legacy, legacyMatch);
|
||||
|
||||
return (MatchType.None, Match.Empty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否可以解析该行(任意一行)
|
||||
/// </summary>
|
||||
public bool CanParse(string line) => TryMatch(line).Type != MatchType.None;
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为多行记录的起始行(header行)
|
||||
/// </summary>
|
||||
public bool IsMultiLineStart(string line) => TryMatch(line).Type == MatchType.Header;
|
||||
|
||||
/// <summary>
|
||||
/// 单行解析(兼容旧格式)
|
||||
/// </summary>
|
||||
public ParseResult<PlossRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var (type, match) = TryMatch(line);
|
||||
|
||||
return type switch
|
||||
{
|
||||
MatchType.Legacy => ParseLegacyFormat(match, scenarioId),
|
||||
MatchType.Fod => ParseFodLine(match, scenarioId),
|
||||
MatchType.Header => ParseHeaderLine(match, scenarioId),
|
||||
_ => 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 || lines[0] == null || lines[1] == null)
|
||||
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,
|
||||
// 第一行字段
|
||||
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);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<PlossRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/QfodParser.cs
Normal file
56
src/Gpulse.WCT.DataAnalyzer.Core/Parsers/QfodParser.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Gpulse.WCT.DataAnalyzer.Core.Models;
|
||||
|
||||
namespace Gpulse.WCT.DataAnalyzer.Core.Parsers;
|
||||
|
||||
/// <summary>
|
||||
/// Qfod日志解析器
|
||||
/// 格式: X#:Q A->B C D E F
|
||||
/// 字段映射: X=ChargerIndex, A=CoilIndex, B=DeltaQ, C=CurrentQ, D=RawQ, E=FodType, F=FieldF
|
||||
/// 示例: 0#:Q 1->25 45.5 20.5 2 0
|
||||
/// </summary>
|
||||
public class QfodParser : IParser<QfodRecord>
|
||||
{
|
||||
private static readonly Regex QfodPattern = new(
|
||||
@"^(\d)#:Q\s+(\d+)->(-?\d+)\s+([-\d.]+)\s+([-\d.]+)\s+(\d+)\s+(\d+)$",
|
||||
RegexOptions.Compiled
|
||||
);
|
||||
|
||||
public bool CanParse(string line) => QfodPattern.IsMatch(line);
|
||||
|
||||
public ParseResult<QfodRecord> Parse(string line, Guid scenarioId)
|
||||
{
|
||||
var match = QfodPattern.Match(line);
|
||||
if (!match.Success)
|
||||
{
|
||||
return ParseResult<QfodRecord>.Failure(
|
||||
"FORMAT_MISMATCH",
|
||||
"Line does not match Qfod pattern"
|
||||
);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var record = new QfodRecord
|
||||
{
|
||||
ScenarioId = scenarioId,
|
||||
ChargerIndex = byte.Parse(match.Groups[1].Value),
|
||||
CoilIndex = byte.Parse(match.Groups[2].Value),
|
||||
DeltaQ = int.Parse(match.Groups[3].Value),
|
||||
CurrentQ = float.Parse(match.Groups[4].Value),
|
||||
RawQ = float.Parse(match.Groups[5].Value),
|
||||
FodType = byte.Parse(match.Groups[6].Value),
|
||||
FieldF = byte.Parse(match.Groups[7].Value)
|
||||
};
|
||||
|
||||
return ParseResult<QfodRecord>.Success(record);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return ParseResult<QfodRecord>.Failure(
|
||||
"TYPE_CONVERSION",
|
||||
$"Failed to convert field: {ex.Message}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user