You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1133 lines
29 KiB
1133 lines
29 KiB
using System.Collections.Immutable;
|
|
using System.Text.RegularExpressions;
|
|
using Newtonsoft.Json;
|
|
|
|
namespace LTEMvcApp.Models;
|
|
|
|
/// <summary>
|
|
/// LTE客户端类 - 对应JavaScript中的lte.client对象
|
|
/// </summary>
|
|
public class LTEClient
|
|
{
|
|
#region 常量定义
|
|
|
|
/// <summary>
|
|
/// HFN回绕阈值
|
|
/// </summary>
|
|
private const int HFN_WRAP_THRESHOLD = 512;
|
|
|
|
/// <summary>
|
|
/// 最大日志数量
|
|
/// </summary>
|
|
private const int LOGS_MAX = 2000000;
|
|
|
|
#endregion
|
|
|
|
#region 正则表达式
|
|
|
|
/// <summary>
|
|
/// PHY层日志正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpPhy = new(@"^([a-f0-9\-]+)\s+([a-f0-9\-]+)\s+([\d\.\-]+) (\w+): (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 信息1正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpInfo1 = new(@"^([\w\-]+): (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 信息2正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpInfo2 = new(@"^([\w]+) (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// IP日志正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpIP = new(@"^(len=\d+)\s+(\S+)\s+(.*)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// IPsec日志正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpIPsec = new(@"^len=(\d+)\s+(.*)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// SDU长度正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpSDULen = new(@"SDU_len=(\d+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// SIP日志正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpSIP = new(@"^([:\.\[\]\da-f]+)\s+(\S+) (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 媒体请求正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpMediaReq = new(@"^(\S+) (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 信号记录正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpSignalRecord = new(@"Link:\s([\w\d]+)@(\d+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 小区ID正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpCellID = new(@"^([a-f0-9\-]+) (.+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// RRC UE ID正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpRRC_UE_ID = new(@"Changing UE_ID to 0x(\d+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// RRC TMSI正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpRRC_TMSI = new(@"(5G|m)-TMSI '([\dA-F]+)'H", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// RRC新ID正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpRRC_NEW_ID = new(@"newUE-Identity (['\dA-FH]+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// RRC CRNTI正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpRRC_CRNTI = new(@"c-RNTI '([\dA-F]+)'H", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// NAS TMSI正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpNAS_TMSI = new(@"m-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// NAS 5G TMSI正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpNAS_5GTMSI = new(@"5G-TMSI = 0x([\da-f]+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// RRC频段组合正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpRRC_BC = new(@"(EUTRA|MRDC|NR|NRDC) band combinations", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// PDCCH正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpPDCCH = new(@"^\s*(.+)=(\d+)$", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// S1/NGAP正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpS1NGAP = new(@"^([\da-f\-]+)\s+([\da-f\-]+) (([^\s]+) .+)", RegexOptions.Compiled);
|
|
|
|
/// <summary>
|
|
/// 十六进制转储正则表达式
|
|
/// </summary>
|
|
private static readonly Regex RegExpHexDump = new(@"^[\da-f]+:(\s+[\da-f]{2}){1,16}\s+.{1,16}$", RegexOptions.Compiled);
|
|
|
|
#endregion
|
|
|
|
#region 基础属性
|
|
|
|
/// <summary>
|
|
/// 客户端ID
|
|
/// </summary>
|
|
public int ClientId { get; set; }
|
|
|
|
/// <summary>
|
|
/// 客户端名称
|
|
/// </summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 客户端配置
|
|
/// </summary>
|
|
public ClientConfig Config { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 客户端状态
|
|
/// </summary>
|
|
public ClientState State { get; set; } = ClientState.Stop;
|
|
|
|
/// <summary>
|
|
/// 版本信息
|
|
/// </summary>
|
|
public string? Version { get; set; }
|
|
|
|
/// <summary>
|
|
/// 许可证信息
|
|
/// </summary>
|
|
public string? License { get; set; }
|
|
|
|
/// <summary>
|
|
/// 模型类型
|
|
/// </summary>
|
|
public string? Model { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否有信号记录
|
|
/// </summary>
|
|
public bool HasSignalRecord { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region 日志相关属性
|
|
|
|
/// <summary>
|
|
/// 日志列表
|
|
/// </summary>
|
|
public List<LTELog> Logs { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 日志数量
|
|
/// </summary>
|
|
public int LogCount => Logs.Count;
|
|
|
|
/// <summary>
|
|
/// 是否已过滤
|
|
/// </summary>
|
|
public bool Filtered { get; set; }
|
|
|
|
/// <summary>
|
|
/// 重置标志
|
|
/// </summary>
|
|
public bool ResetFlag { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region 解析器状态
|
|
|
|
/// <summary>
|
|
/// 最后HARQ信息
|
|
/// </summary>
|
|
public Dictionary<int, Dictionary<int, LTELog>> LastHarq { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 最后NB HARQ信息
|
|
/// </summary>
|
|
public Dictionary<int, Dictionary<int, List<LTELog>>> LastNbHarq { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 帧信息
|
|
/// </summary>
|
|
public FrameInfo Frame { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// TMSI到UE ID的映射
|
|
/// </summary>
|
|
public Dictionary<int, int> TmsiToUeId { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// RNTI到UE ID的映射
|
|
/// </summary>
|
|
public Dictionary<int, int> RntiToUeId { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// UE列表
|
|
/// </summary>
|
|
public Dictionary<int, UEInfo> UeList { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 最后时间戳
|
|
/// </summary>
|
|
public long LastTimestamp { get; set; }
|
|
|
|
/// <summary>
|
|
/// 时间戳偏移
|
|
/// </summary>
|
|
public long TimestampOffset { get; set; }
|
|
|
|
/// <summary>
|
|
/// 最后小区
|
|
/// </summary>
|
|
public int? LastCell { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region 功能标志
|
|
|
|
/// <summary>
|
|
/// 是否有小区信息
|
|
/// </summary>
|
|
public bool HasCell { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否有物理层信息
|
|
/// </summary>
|
|
public bool HasPhy { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否有数据
|
|
/// </summary>
|
|
public bool HasData { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否有RNTI
|
|
/// </summary>
|
|
public bool HasRnti { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否有资源块
|
|
/// </summary>
|
|
public bool HasRb { get; set; }
|
|
|
|
#endregion
|
|
|
|
#region 参数和组件
|
|
|
|
/// <summary>
|
|
/// 参数信息
|
|
/// </summary>
|
|
public Dictionary<string, object> Parameters { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 组件列表
|
|
/// </summary>
|
|
public Dictionary<string, object> Components { get; set; } = new();
|
|
|
|
#endregion
|
|
|
|
#region 解析器私有字段
|
|
|
|
/// <summary>
|
|
/// 字符串到ID的映射缓存
|
|
/// </summary>
|
|
private readonly Dictionary<string, int> _stringToIdCache = new();
|
|
|
|
/// <summary>
|
|
/// ID到字符串的映射缓存
|
|
/// </summary>
|
|
private readonly Dictionary<int, string> _idToStringCache = new();
|
|
|
|
/// <summary>
|
|
/// 字符串ID计数器
|
|
/// </summary>
|
|
private int _stringIdCounter = 0;
|
|
|
|
#endregion
|
|
|
|
#region 构造函数
|
|
|
|
public LTEClient(ClientConfig config)
|
|
{
|
|
Config = config;
|
|
Name = config.Name;
|
|
ClientId = GenerateClientId();
|
|
ResetParserState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 使用名称创建客户端
|
|
/// </summary>
|
|
/// <param name="name">客户端名称</param>
|
|
public LTEClient(string name)
|
|
{
|
|
Config = new ClientConfig { Name = name, Enabled = true };
|
|
Name = name;
|
|
ClientId = GenerateClientId();
|
|
ResetParserState();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 客户端控制方法
|
|
|
|
/// <summary>
|
|
/// 启动客户端
|
|
/// </summary>
|
|
public void Start()
|
|
{
|
|
SetState(ClientState.Start);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 停止客户端
|
|
/// </summary>
|
|
public void Stop()
|
|
{
|
|
SetState(ClientState.Stop);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 销毁客户端
|
|
/// </summary>
|
|
public void Destroy()
|
|
{
|
|
SetState(ClientState.Destroy);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 日志管理方法
|
|
|
|
/// <summary>
|
|
/// 重置日志
|
|
/// </summary>
|
|
public void ResetLogs()
|
|
{
|
|
if (LogCount > 0)
|
|
{
|
|
ResetFlag = true;
|
|
Logs.Clear();
|
|
ResetParserState();
|
|
HasCell = false;
|
|
HasPhy = false;
|
|
HasData = false;
|
|
HasRnti = false;
|
|
HasRb = false;
|
|
HasSignalRecord = false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加日志
|
|
/// </summary>
|
|
public void AddLog(LTELog log)
|
|
{
|
|
// 检查时间戳回绕
|
|
var timestamp = log.Timestamp + TimestampOffset;
|
|
if (timestamp < LastTimestamp - 100)
|
|
{
|
|
Console.WriteLine($"Log wrap by {LastTimestamp - timestamp}");
|
|
timestamp += 86400000; // 24小时
|
|
TimestampOffset += 86400000;
|
|
}
|
|
LastTimestamp = log.Timestamp = timestamp;
|
|
|
|
log.Client = this;
|
|
log.Id = GenerateLogId();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 工具方法
|
|
|
|
/// <summary>
|
|
/// 设置头信息
|
|
/// </summary>
|
|
public void SetHeaders(string[] headers)
|
|
{
|
|
// 实现头信息设置逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 初始化模型猜测
|
|
/// </summary>
|
|
public void LogModelGuessInit()
|
|
{
|
|
// 实现模型猜测初始化逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 模型猜测
|
|
/// </summary>
|
|
public void LogModelGuess(List<Dictionary<string, object>> logs)
|
|
{
|
|
// 实现模型猜测逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 方向转换
|
|
/// </summary>
|
|
public int DirConvert(LTELog log)
|
|
{
|
|
// 实现方向转换逻辑
|
|
return 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 字符串转ID
|
|
/// </summary>
|
|
public int StringToId(string str)
|
|
{
|
|
if (_stringToIdCache.TryGetValue(str, out var id))
|
|
{
|
|
return id;
|
|
}
|
|
|
|
id = ++_stringIdCounter;
|
|
_stringToIdCache[str] = id;
|
|
_idToStringCache[id] = str;
|
|
return id;
|
|
}
|
|
|
|
/// <summary>
|
|
/// ID转字符串
|
|
/// </summary>
|
|
public string IdToString(int id)
|
|
{
|
|
return _idToStringCache.TryGetValue(id, out var str) ? str : "";
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 日志解析方法
|
|
|
|
/// <summary>
|
|
/// 解析日志列表 - 对应JavaScript的_logListParse方法
|
|
/// </summary>
|
|
/// <param name="logs">日志列表</param>
|
|
/// <param name="isWebSocket">是否为WebSocket消息</param>
|
|
public void ParseLogList(List<LTELog> logs, bool isWebSocket = false)
|
|
{
|
|
var length = logs.Count;
|
|
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
var log = logs[i];
|
|
log.Message = log.Message + "";
|
|
|
|
AddLog(log);
|
|
|
|
// 解析消息
|
|
switch (log.Layer)
|
|
{
|
|
case "PHY":
|
|
if (!ParsePhyLog(log, log.Message, isWebSocket)) continue;
|
|
ProcessPhyLog(log);
|
|
break;
|
|
|
|
case "RRC":
|
|
ParseCellId(log, isWebSocket);
|
|
var rrcUeIdMatch = RegExpRRC_UE_ID.Match(log.Message);
|
|
if (rrcUeIdMatch.Success)
|
|
{
|
|
SetSameUe(log, int.Parse(rrcUeIdMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber));
|
|
continue;
|
|
}
|
|
|
|
var infoMatch = RegExpInfo1.Match(log.Message);
|
|
if (infoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, infoMatch.Groups[1].Value)) continue;
|
|
log.Message = infoMatch.Groups[2].Value;
|
|
ProcessRrcLog(log);
|
|
}
|
|
|
|
var bcMatch = RegExpRRC_BC.Match(log.Message);
|
|
if (bcMatch.Success)
|
|
{
|
|
try
|
|
{
|
|
var data = log.GetDataString();
|
|
var jsonData = JsonConvert.DeserializeObject<object>(data);
|
|
// 处理频段组合信息
|
|
}
|
|
catch
|
|
{
|
|
// 忽略JSON解析错误
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "NAS":
|
|
ParseCellId(log, isWebSocket);
|
|
var nasTmsiMatch = RegExpNAS_TMSI.Match(log.Message);
|
|
if (nasTmsiMatch.Success)
|
|
{
|
|
SetTmsi(log, nasTmsiMatch.Groups[1].Value);
|
|
continue;
|
|
}
|
|
|
|
var nas5gTmsiMatch = RegExpNAS_5GTMSI.Match(log.Message);
|
|
if (nas5gTmsiMatch.Success)
|
|
{
|
|
SetTmsi(log, nas5gTmsiMatch.Groups[1].Value);
|
|
continue;
|
|
}
|
|
|
|
var nasInfoMatch = RegExpInfo2.Match(log.Message);
|
|
if (nasInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, nasInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = nasInfoMatch.Groups[2].Value;
|
|
ProcessNasLog(log);
|
|
}
|
|
break;
|
|
|
|
case "MAC":
|
|
ParseCellId(log, isWebSocket);
|
|
ParseMacLog(log);
|
|
break;
|
|
|
|
case "IP":
|
|
var ipMatch = RegExpIP.Match(log.Message);
|
|
if (ipMatch.Success)
|
|
{
|
|
var lenPart = ipMatch.Groups[1].Value;
|
|
log.IpLen = int.Parse(lenPart.Split('=')[1]);
|
|
if (!SetLogInfo(log, ipMatch.Groups[2].Value)) continue;
|
|
log.Message = ipMatch.Groups[3].Value;
|
|
HasData = true;
|
|
}
|
|
break;
|
|
|
|
case "GTPU":
|
|
var sduLenMatch = RegExpSDULen.Match(log.Message);
|
|
if (sduLenMatch.Success)
|
|
{
|
|
log.SduLen = int.Parse(sduLenMatch.Groups[1].Value);
|
|
HasData = true;
|
|
}
|
|
break;
|
|
|
|
case "S1AP":
|
|
case "NGAP":
|
|
var s1ngMatch = RegExpS1NGAP.Match(log.Message);
|
|
if (s1ngMatch.Success)
|
|
{
|
|
log.Message = s1ngMatch.Groups[3].Value;
|
|
var coreId = int.TryParse(s1ngMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber, null, out var core) ? core : (int?)null;
|
|
var ranId = int.TryParse(s1ngMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber, null, out var ran) ? ran : (int?)null;
|
|
|
|
log.LinkIds = new LinkIds { Core = coreId, Ran = ranId };
|
|
}
|
|
break;
|
|
|
|
case "SIP":
|
|
var sipMatch = RegExpSIP.Match(log.Message);
|
|
if (sipMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, sipMatch.Groups[2].Value)) continue;
|
|
log.Message = sipMatch.Groups[3].Value;
|
|
}
|
|
break;
|
|
|
|
case "MEDIA":
|
|
var mediaMatch = RegExpMediaReq.Match(log.Message);
|
|
if (mediaMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, mediaMatch.Groups[1].Value)) continue;
|
|
log.Message = mediaMatch.Groups[2].Value;
|
|
}
|
|
break;
|
|
|
|
case "IPsec":
|
|
var ipsecMatch = RegExpIPsec.Match(log.Message);
|
|
if (ipsecMatch.Success)
|
|
{
|
|
log.IpLen = int.Parse(ipsecMatch.Groups[1].Value);
|
|
log.Message = ipsecMatch.Groups[2].Value;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 处理提示信息
|
|
ProcessHints(log);
|
|
}
|
|
|
|
// 限制日志数量
|
|
if (LogCount > LOGS_MAX)
|
|
{
|
|
var excess = LogCount - LOGS_MAX;
|
|
Logs.RemoveRange(0, excess);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region PHY层解析方法
|
|
|
|
/// <summary>
|
|
/// 解析PHY日志
|
|
/// </summary>
|
|
private bool ParsePhyLog(LTELog log, string message, bool isWebSocket)
|
|
{
|
|
if (!isWebSocket)
|
|
{
|
|
var phyMatch = RegExpPhy.Match(message);
|
|
if (!phyMatch.Success)
|
|
{
|
|
Console.WriteLine($"Bad PHY log: {log}");
|
|
return false;
|
|
}
|
|
|
|
log.Cell = int.Parse(phyMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
|
|
log.Rnti = int.Parse(phyMatch.Groups[2].Value, System.Globalization.NumberStyles.HexNumber);
|
|
log.Channel = phyMatch.Groups[4].Value;
|
|
log.Message = phyMatch.Groups[5].Value;
|
|
}
|
|
|
|
HasPhy = true;
|
|
HasCell = true;
|
|
HasRnti = true;
|
|
|
|
// 解析PHY参数
|
|
var lines = log.GetData();
|
|
foreach (var line in lines)
|
|
{
|
|
var parts = line.Split('=');
|
|
if (parts.Length == 2)
|
|
{
|
|
ParsePhyParameter(log, parts[0].Trim(), parts[1].Trim());
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析PHY参数
|
|
/// </summary>
|
|
private void ParsePhyParameter(LTELog log, string param, string value)
|
|
{
|
|
switch (param.ToLower())
|
|
{
|
|
case "frame":
|
|
if (int.TryParse(value, out var frame))
|
|
{
|
|
log.Frame = frame;
|
|
HasPhy = true;
|
|
}
|
|
break;
|
|
case "subframe":
|
|
if (int.TryParse(value, out var subframe))
|
|
{
|
|
log.Subframe = subframe;
|
|
}
|
|
break;
|
|
case "slot":
|
|
if (int.TryParse(value, out var slot))
|
|
{
|
|
log.Slot = slot;
|
|
}
|
|
break;
|
|
case "symbol":
|
|
if (int.TryParse(value, out var symbol))
|
|
{
|
|
log.Symbol = symbol;
|
|
}
|
|
break;
|
|
case "ant":
|
|
if (int.TryParse(value, out var ant))
|
|
{
|
|
log.AntennaPort = ant;
|
|
}
|
|
break;
|
|
case "rb_start":
|
|
if (int.TryParse(value, out var rbStart))
|
|
{
|
|
log.RbStart = rbStart;
|
|
HasRb = true;
|
|
}
|
|
break;
|
|
case "rb_count":
|
|
if (int.TryParse(value, out var rbCount))
|
|
{
|
|
log.RbCount = rbCount;
|
|
HasRb = true;
|
|
}
|
|
break;
|
|
case "mcs":
|
|
if (int.TryParse(value, out var mcs))
|
|
{
|
|
log.Mcs = mcs;
|
|
}
|
|
break;
|
|
case "tbs":
|
|
if (int.TryParse(value, out var tbs))
|
|
{
|
|
log.Tbs = tbs;
|
|
}
|
|
break;
|
|
case "harq_id":
|
|
if (int.TryParse(value, out var harqId))
|
|
{
|
|
log.HarqId = harqId;
|
|
}
|
|
break;
|
|
case "harq_ndi":
|
|
if (bool.TryParse(value, out var harqNdi))
|
|
{
|
|
log.HarqNdi = harqNdi;
|
|
}
|
|
break;
|
|
case "harq_rv":
|
|
if (int.TryParse(value, out var harqRv))
|
|
{
|
|
log.HarqRedundancyVersion = harqRv;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理PHY日志
|
|
/// </summary>
|
|
private void ProcessPhyLog(LTELog log)
|
|
{
|
|
// 处理HARQ信息
|
|
if (log.HarqId.HasValue)
|
|
{
|
|
ProcessHarqLog(log, log.HarqId.Value);
|
|
}
|
|
|
|
// 处理帧信息
|
|
if (log.Frame.HasValue)
|
|
{
|
|
var frame = log.Frame.Value;
|
|
var hfn = frame >> 10;
|
|
var sfn = frame & 0x3FF;
|
|
|
|
if (Frame.Timestamp == -1)
|
|
{
|
|
Frame.Timestamp = log.Timestamp;
|
|
Frame.Hfn = hfn;
|
|
Frame.Last = sfn;
|
|
}
|
|
else
|
|
{
|
|
var diff = sfn - Frame.Last;
|
|
if (diff < -HFN_WRAP_THRESHOLD)
|
|
{
|
|
Frame.Hfn++;
|
|
}
|
|
else if (diff > HFN_WRAP_THRESHOLD)
|
|
{
|
|
Frame.Hfn--;
|
|
}
|
|
|
|
Frame.Last = sfn;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理HARQ日志
|
|
/// </summary>
|
|
private void ProcessHarqLog(LTELog log, int harq)
|
|
{
|
|
if (log.Cell.HasValue && log.UeId.HasValue)
|
|
{
|
|
var cell = log.Cell.Value;
|
|
var ueId = log.UeId.Value;
|
|
var key = ueId * 32 + harq;
|
|
|
|
if (!LastHarq.ContainsKey(cell))
|
|
{
|
|
LastHarq[cell] = new Dictionary<int, LTELog>();
|
|
}
|
|
|
|
LastHarq[cell][key] = log;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 其他层解析方法
|
|
|
|
/// <summary>
|
|
/// 解析小区ID
|
|
/// </summary>
|
|
private void ParseCellId(LTELog log, bool isWebSocket)
|
|
{
|
|
if (!isWebSocket)
|
|
{
|
|
var cellMatch = RegExpCellID.Match(log.Message);
|
|
if (cellMatch.Success)
|
|
{
|
|
log.Cell = int.Parse(cellMatch.Groups[1].Value, System.Globalization.NumberStyles.HexNumber);
|
|
log.Message = cellMatch.Groups[2].Value;
|
|
HasCell = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置日志信息
|
|
/// </summary>
|
|
private bool SetLogInfo(LTELog log, string info)
|
|
{
|
|
if (string.IsNullOrEmpty(info) || info == "?")
|
|
return false;
|
|
|
|
log.Info = StringToId(info);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置相同UE
|
|
/// </summary>
|
|
private void SetSameUe(LTELog log, int ueId)
|
|
{
|
|
log.UeId = ueId;
|
|
if (!UeList.ContainsKey(ueId))
|
|
{
|
|
UeList[ueId] = new UEInfo { UeId = ueId };
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理RRC日志
|
|
/// </summary>
|
|
private void ProcessRrcLog(LTELog log)
|
|
{
|
|
// 处理RRC TMSI
|
|
var tmsiMatch = RegExpRRC_TMSI.Match(log.Message);
|
|
if (tmsiMatch.Success)
|
|
{
|
|
SetTmsi(log, tmsiMatch.Groups[2].Value);
|
|
}
|
|
|
|
// 处理RRC CRNTI
|
|
var crntiMatch = RegExpRRC_CRNTI.Match(log.Message);
|
|
if (crntiMatch.Success)
|
|
{
|
|
SetRnti(log, crntiMatch.Groups[1].Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理NAS日志
|
|
/// </summary>
|
|
private void ProcessNasLog(LTELog log)
|
|
{
|
|
// NAS日志处理逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析MAC日志
|
|
/// </summary>
|
|
private void ParseMacLog(LTELog log)
|
|
{
|
|
// MAC日志解析逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理提示信息
|
|
/// </summary>
|
|
private void ProcessHints(LTELog log, Dictionary<string, object>? config = null)
|
|
{
|
|
// 处理提示信息逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置TMSI
|
|
/// </summary>
|
|
private void SetTmsi(LTELog log, string tmsi)
|
|
{
|
|
var tmsiId = int.Parse(tmsi, System.Globalization.NumberStyles.HexNumber);
|
|
if (log.UeId.HasValue)
|
|
{
|
|
TmsiToUeId[tmsiId] = log.UeId.Value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置RNTI
|
|
/// </summary>
|
|
private void SetRnti(LTELog log, string rnti)
|
|
{
|
|
var rntiId = int.Parse(rnti, System.Globalization.NumberStyles.HexNumber);
|
|
if (log.UeId.HasValue)
|
|
{
|
|
RntiToUeId[rntiId] = log.UeId.Value;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 私有辅助方法
|
|
|
|
/// <summary>
|
|
/// 设置状态
|
|
/// </summary>
|
|
private void SetState(ClientState state)
|
|
{
|
|
if (State != state)
|
|
{
|
|
State = state;
|
|
switch (state)
|
|
{
|
|
case ClientState.Stop:
|
|
ResetLogs();
|
|
break;
|
|
case ClientState.Start:
|
|
case ClientState.Connected:
|
|
break;
|
|
case ClientState.Destroy:
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 重置解析器状态
|
|
/// </summary>
|
|
private void ResetParserState()
|
|
{
|
|
LastHarq.Clear();
|
|
LastNbHarq.Clear();
|
|
Frame = new FrameInfo();
|
|
TmsiToUeId.Clear();
|
|
RntiToUeId.Clear();
|
|
UeList.Clear();
|
|
LastTimestamp = 0;
|
|
TimestampOffset = 0;
|
|
LastCell = null;
|
|
|
|
// 重置解析器缓存
|
|
_stringToIdCache.Clear();
|
|
_idToStringCache.Clear();
|
|
_stringIdCounter = 0;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成客户端ID
|
|
/// </summary>
|
|
private static int GenerateClientId()
|
|
{
|
|
return Interlocked.Increment(ref _clientIdCounter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 生成日志ID
|
|
/// </summary>
|
|
private static int GenerateLogId()
|
|
{
|
|
return Interlocked.Increment(ref _logIdCounter);
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 静态字段
|
|
|
|
private static int _clientIdCounter = 0;
|
|
private static int _logIdCounter = 0;
|
|
|
|
#endregion
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户端日志配置
|
|
/// </summary>
|
|
public class ClientLogsConfig
|
|
{
|
|
/// <summary>
|
|
/// 日志层配置
|
|
/// </summary>
|
|
public Dictionary<string, LogLayerConfig> Layers { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 是否启用信号日志
|
|
/// </summary>
|
|
public bool? Signal { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否启用控制信道日志
|
|
/// </summary>
|
|
public bool? Cch { get; set; }
|
|
|
|
// 允许其他未明确定义的属性
|
|
[JsonExtensionData]
|
|
public Dictionary<string, object>? ExtensionData { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户端配置
|
|
/// </summary>
|
|
public class ClientConfig
|
|
{
|
|
/// <summary>
|
|
/// 客户端名称
|
|
/// </summary>
|
|
public string Name { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 服务器地址
|
|
/// </summary>
|
|
public string Address { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 是否启用
|
|
/// </summary>
|
|
public bool Enabled { get; set; }
|
|
|
|
/// <summary>
|
|
/// 密码
|
|
/// </summary>
|
|
public string Password { get; set; } = string.Empty;
|
|
|
|
/// <summary>
|
|
/// 重连延迟(毫秒)
|
|
/// </summary>
|
|
public int ReconnectDelay { get; set; } = 5000;
|
|
|
|
/// <summary>
|
|
/// 是否启用SSL
|
|
/// </summary>
|
|
public bool Ssl { get; set; }
|
|
|
|
/// <summary>
|
|
/// 日志配置
|
|
/// </summary>
|
|
public ClientLogsConfig Logs { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 是否暂停
|
|
/// </summary>
|
|
public bool Pause { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否只读
|
|
/// </summary>
|
|
public bool Readonly { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否跳过日志菜单
|
|
/// </summary>
|
|
public bool SkipLogMenu { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否锁定
|
|
/// </summary>
|
|
public bool Locked { get; set; }
|
|
|
|
/// <summary>
|
|
/// 是否激活
|
|
/// </summary>
|
|
public bool Active { get; set; }
|
|
|
|
/// <summary>
|
|
/// 模型
|
|
/// </summary>
|
|
public string? Model { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户端状态
|
|
/// </summary>
|
|
public enum ClientState
|
|
{
|
|
Stop,
|
|
Start,
|
|
Loading,
|
|
Connecting,
|
|
Connected,
|
|
Error,
|
|
Destroy
|
|
}
|
|
|
|
/// <summary>
|
|
/// 帧信息
|
|
/// </summary>
|
|
public class FrameInfo
|
|
{
|
|
public int Last { get; set; }
|
|
public int Hfn { get; set; }
|
|
public long Timestamp { get; set; } = -1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// UE信息
|
|
/// </summary>
|
|
public class UEInfo
|
|
{
|
|
public int UeId { get; set; }
|
|
public string? Imsi { get; set; }
|
|
public string? Imei { get; set; }
|
|
public object? Caps { get; set; }
|
|
}
|