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.
2325 lines
68 KiB
2325 lines
68 KiB
using System.Collections.Immutable;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// BSR表 - 对应JavaScript中的_bsr_table
|
|
/// </summary>
|
|
private static readonly int[] BsrTable = {
|
|
0, 10, 12, 14, 17, 19, 22, 26, 31, 36, 42, 49, 57, 67, 78, 91, 107, 125, 146, 171, 200, 234, 274, 321, 376, 440, 515, 603, 706, 826, 967, 1132, 1326, 1552, 1817, 2127, 2490, 2915, 3413, 3995, 4677, 5476, 6411, 7505, 8787, 10287, 12043, 14099, 16507, 19325, 22624, 26487, 31009, 36304, 42502, 49759, 58255, 68201, 79846, 93479, 109439, 128125, 150000, 500000
|
|
};
|
|
|
|
#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; }
|
|
|
|
/// <summary>
|
|
/// 头部信息
|
|
/// </summary>
|
|
public string[]? Headers { get; set; }
|
|
|
|
/// <summary>
|
|
/// 模式(ran/ims)
|
|
/// </summary>
|
|
public string Mode { get; set; } = "ran";
|
|
|
|
#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 参数和组件
|
|
|
|
#region
|
|
private int PDSCH;
|
|
private int PDCCH;
|
|
private int EPDCCH;
|
|
private int PUSCH;
|
|
private int PUCCH;
|
|
private int NPDSCH;
|
|
private int NPUSCH;
|
|
private int NPBCH;
|
|
private int BCCH_NR;
|
|
private int ID_HINTS;
|
|
private int SRS;
|
|
private int CSI;
|
|
private int SIM_EVENT;
|
|
private int IP_SIM;
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 参数信息
|
|
/// </summary>
|
|
public Dictionary<string, object> Parameters { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 组件列表
|
|
/// </summary>
|
|
public Dictionary<string, object> Components { get; set; } = new();
|
|
|
|
#endregion
|
|
|
|
#region 依赖注入
|
|
|
|
/// <summary>
|
|
/// 日志管理器
|
|
/// </summary>
|
|
private readonly LogsManager _lteLogs;
|
|
|
|
/// <summary>
|
|
/// 获取日志管理器实例
|
|
/// </summary>
|
|
public LogsManager LogsManager => _lteLogs;
|
|
|
|
#endregion
|
|
|
|
#region 构造函数
|
|
|
|
public LTEClient(ClientConfig config, LogsManager logsManager)
|
|
{
|
|
Config = config;
|
|
Name = config.Name;
|
|
ClientId = GenerateClientId();
|
|
_lteLogs = logsManager;
|
|
ResetParserState();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 使用名称创建客户端
|
|
/// </summary>
|
|
/// <param name="name">客户端名称</param>
|
|
/// <param name="logsManager">日志管理器</param>
|
|
public LTEClient(string name, LogsManager logsManager)
|
|
{
|
|
Config = new ClientConfig { Name = name, Enabled = true };
|
|
Name = name;
|
|
ClientId = GenerateClientId();
|
|
_lteLogs = logsManager;
|
|
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>
|
|
/// 设置头信息 - 对应JavaScript的setHeaders方法
|
|
/// </summary>
|
|
/// <param name="headers">头信息数组</param>
|
|
public void SetHeaders(string[] headers)
|
|
{
|
|
// 保存头信息
|
|
Headers = headers;
|
|
var cells = new List<Dictionary<string, object>>();
|
|
|
|
for (int i = 0; i < headers.Length; i++)
|
|
{
|
|
var header = headers[i];
|
|
Match? info;
|
|
|
|
// 检查版本信息
|
|
info = Regex.Match(header, @"lte(\w+) version ([\d-]+)", RegexOptions.IgnoreCase);
|
|
if (info.Success)
|
|
{
|
|
if (string.IsNullOrEmpty(Config.Model))
|
|
{
|
|
var model = info.Groups[1].Value.ToUpper();
|
|
if (_clientModelConfigs.ContainsKey(model))
|
|
{
|
|
SetModel(model);
|
|
}
|
|
}
|
|
Version = info.Groups[2].Value;
|
|
}
|
|
// 检查许可证信息
|
|
else if (Regex.IsMatch(header, @"Licensed to"))
|
|
{
|
|
License = header;
|
|
}
|
|
// 检查元数据信息
|
|
else if ((info = Regex.Match(header, @"Metadata:(.+)$")).Success)
|
|
{
|
|
try
|
|
{
|
|
var metadata = JsonConvert.DeserializeObject<Dictionary<string, object>>(info.Groups[1].Value);
|
|
if (metadata.TryGetValue("model", out var model))
|
|
{
|
|
SetModel(model.ToString());
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// 忽略JSON解析错误
|
|
}
|
|
}
|
|
// 检查RAN ID信息
|
|
else if ((info = Regex.Match(header, @"(global_(ran_node|enb)_id)=([\d\.]+)")).Success)
|
|
{
|
|
SetModel("ENB");
|
|
SetRanId(info.Groups[3].Value);
|
|
}
|
|
// 检查小区信息
|
|
else if ((info = Regex.Match(header, @"Cell 0x(\d+): (.*)")).Success)
|
|
{
|
|
var cell = new Dictionary<string, object>
|
|
{
|
|
["cell_id"] = int.Parse(info.Groups[1].Value, System.Globalization.NumberStyles.HexNumber),
|
|
["sib1Decoded"] = true,
|
|
["sib2Decoded"] = true
|
|
};
|
|
|
|
var list = info.Groups[2].Value.Split(' ');
|
|
foreach (var param in list)
|
|
{
|
|
var parts = param.Split('=');
|
|
if (parts.Length == 2)
|
|
{
|
|
cell[parts[0]] = HeaderCellParam(parts[0], parts[1]);
|
|
}
|
|
}
|
|
|
|
cells.Add(cell);
|
|
}
|
|
// 检查小区参数信息
|
|
else if (cells.Count > 0)
|
|
{
|
|
info = Regex.Match(header, @"([UD]L): (.*)");
|
|
if (info.Success)
|
|
{
|
|
var cell = cells[cells.Count - 1];
|
|
var dir = info.Groups[1].Value;
|
|
var list = info.Groups[2].Value.Split(' ');
|
|
foreach (var param in list)
|
|
{
|
|
var parts = param.Split('=');
|
|
if (parts.Length == 2)
|
|
{
|
|
cell[parts[0]] = HeaderCellParam(parts[0], parts[1]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 添加所有小区
|
|
foreach (var cell in cells)
|
|
{
|
|
var cellId = (int)cell["cell_id"];
|
|
AddCell(cellId, cell);
|
|
}
|
|
|
|
// 通知日志管理器刷新客户端网格
|
|
_lteLogs.RefreshClientGrid();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 处理头信息中的小区参数 - 对应JavaScript的_headerCellParam方法
|
|
/// </summary>
|
|
/// <param name="param">参数名</param>
|
|
/// <param name="value">参数值</param>
|
|
/// <returns>处理后的值</returns>
|
|
private object HeaderCellParam(string param, string value)
|
|
{
|
|
switch (param)
|
|
{
|
|
case "br_dl_sf_bitmap":
|
|
case "nb_dl_sf_bitmap":
|
|
case "label":
|
|
return value;
|
|
default:
|
|
if (int.TryParse(value, out var intValue))
|
|
return intValue;
|
|
return value;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加小区 - 对应JavaScript的_addCell方法
|
|
/// </summary>
|
|
/// <param name="id">小区ID</param>
|
|
/// <param name="config">小区配置</param>
|
|
/// <returns>小区对象</returns>
|
|
public LTECell AddCell(int id, Dictionary<string, object> config)
|
|
{
|
|
var cell = new LTECell(id, config);
|
|
|
|
if (Parameters.ContainsKey("cells") && Parameters["cells"] is Dictionary<string, LTECell> cells)
|
|
{
|
|
cells[id.ToString()] = cell;
|
|
}
|
|
else
|
|
{
|
|
Parameters["cells"] = new Dictionary<string, LTECell> { [id.ToString()] = cell };
|
|
}
|
|
|
|
return cell;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置RAN ID - 对应JavaScript的setRanId方法
|
|
/// </summary>
|
|
/// <param name="ranId">RAN ID</param>
|
|
public void SetRanId(string ranId)
|
|
{
|
|
RanIds.Add(ranId);
|
|
|
|
// 处理RAN ID链接(简化实现,因为C#版本没有clientList)
|
|
// 在实际应用中,这里可能需要与其他客户端进行协调
|
|
}
|
|
|
|
/// <summary>
|
|
/// 初始化模型猜测
|
|
/// </summary>
|
|
public void LogModelGuessInit()
|
|
{
|
|
if (string.IsNullOrEmpty(Config.Model))
|
|
{
|
|
// 尝试所有模型
|
|
var modelList = new List<string> { "RUE", "UE", "PROBE", "ENB", "N3IWF", "MBMSGW", "MME", "IMS", "LICENSE", "MONITOR" };
|
|
if (!TryModelHint(modelList))
|
|
{
|
|
SetModel("MME"); // 默认模型
|
|
}
|
|
|
|
// 初始化模型猜测字典
|
|
_modelGuess = new Dictionary<string, bool>();
|
|
var layerConfig = _lteLogs.GetLayerConfig();
|
|
foreach (var layer in layerConfig.Keys)
|
|
{
|
|
_modelGuess[layer] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 模型猜测字典
|
|
/// </summary>
|
|
private Dictionary<string, bool>? _modelGuess;
|
|
|
|
/// <summary>
|
|
/// 模型猜测
|
|
/// </summary>
|
|
/// <param name="logs">日志列表</param>
|
|
public void LogModelGuess(List<Dictionary<string, object>> logs)
|
|
{
|
|
var modelGuess = _modelGuess;
|
|
if (modelGuess == null)
|
|
return;
|
|
|
|
// 标记所有出现的层
|
|
foreach (var log in logs)
|
|
{
|
|
if (log.TryGetValue("layer", out var layer) && layer is string layerStr)
|
|
{
|
|
modelGuess[layerStr] = true;
|
|
}
|
|
}
|
|
|
|
// 从 LogsManager 获取层配置
|
|
var layerConfig = _lteLogs.GetLayerConfig();
|
|
var modelList = new List<string> { "RUE", "UE", "PROBE", "ENB", "N3IWF", "MBMSGW", "MME", "IMS", "LICENSE", "MONITOR" };
|
|
var availableModels = new List<string>(modelList);
|
|
|
|
// 根据层配置过滤模型(与 JavaScript 版本保持一致)
|
|
foreach (var layer in modelGuess.Keys)
|
|
{
|
|
if (!modelGuess[layer]) continue;
|
|
|
|
if (layerConfig.TryGetValue(layer, out var config))
|
|
{
|
|
// 使用反射获取 dir 属性
|
|
var configType = config.GetType();
|
|
var dirProperty = configType.GetProperty("dir");
|
|
if (dirProperty != null)
|
|
{
|
|
var dir = dirProperty.GetValue(config) as Dictionary<string, int>;
|
|
if (dir != null)
|
|
{
|
|
// 移除不支持的模型(与 JavaScript 版本逻辑一致)
|
|
for (int j = 0; j < modelList.Count; j++)
|
|
{
|
|
var model = modelList[j];
|
|
if (!dir.ContainsKey(model))
|
|
{
|
|
var idx = availableModels.IndexOf(model);
|
|
if (idx >= 0)
|
|
availableModels.RemoveAt(idx);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 如果只剩下一个模型,直接设置(与 JavaScript 版本一致)
|
|
if (availableModels.Count < 2)
|
|
break;
|
|
}
|
|
|
|
// 设置模型(与 JavaScript 版本一致)
|
|
if (availableModels.Count > 0)
|
|
{
|
|
if (availableModels.Count == 1)
|
|
{
|
|
SetModel(availableModels[0]);
|
|
}
|
|
else
|
|
{
|
|
// 支持多个模型的情况,传递第一个模型
|
|
SetModel(availableModels[0]);
|
|
}
|
|
_modelGuess = null; // 清除猜测字典
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 方向转换 - 对应JavaScript的_dirConvert方法
|
|
/// </summary>
|
|
/// <param name="log">日志对象</param>
|
|
/// <returns>转换后的方向值</returns>
|
|
public int DirConvert(LTELog log)
|
|
{
|
|
// 获取当前层的方向配置
|
|
if (_layerDir.TryGetValue(log.Layer, out var layerDir))
|
|
{
|
|
// 根据日志的原始方向获取转换后的方向
|
|
if (layerDir.TryGetValue(log.Direction.ToString(), out var convertedDir))
|
|
{
|
|
return convertedDir;
|
|
}
|
|
}
|
|
return 0; // 默认返回0(DIR_NONE)
|
|
}
|
|
|
|
/// <summary>
|
|
/// 字符串转ID - 委托给LogsManager
|
|
/// </summary>
|
|
public int StringToId(string str)
|
|
{
|
|
return _lteLogs.String2Id(str);
|
|
}
|
|
|
|
/// <summary>
|
|
/// ID转字符串 - 委托给LogsManager
|
|
/// </summary>
|
|
public string IdToString(int id)
|
|
{
|
|
return _lteLogs.Id2String(id);
|
|
}
|
|
|
|
#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 rrcInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (rrcInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, rrcInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = rrcInfoMatch.Groups[2].Value;
|
|
ProcessRrcLog(log);
|
|
}
|
|
|
|
var rrcBcMatch = RegExpRRC_BC.Match(log.Message);
|
|
if (rrcBcMatch.Success)
|
|
{
|
|
try
|
|
{
|
|
var data = log.GetDataString();
|
|
var jsonData = JsonConvert.DeserializeObject<object>(data);
|
|
// 这里需要调用getUECaps(log.ue_id).setBandComb(data, info[1].toLowerCase());
|
|
// 暂时注释掉,因为还没有实现getUECaps方法
|
|
// GetUECaps(log.UeId ?? 0).SetBandComb(jsonData, rrcBcMatch.Groups[1].Value.ToLower());
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Console.WriteLine($"Band combination parsing error: {e.Message}");
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "NAS":
|
|
var nasInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (nasInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, nasInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = nasInfoMatch.Groups[2].Value;
|
|
ProcessNasLog(log);
|
|
}
|
|
break;
|
|
|
|
case "MON":
|
|
case "OTS":
|
|
var monOtsInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (monOtsInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, monOtsInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = monOtsInfoMatch.Groups[2].Value;
|
|
}
|
|
break;
|
|
|
|
case "MAC":
|
|
ParseCellId(log, isWebSocket);
|
|
ParseMacLog(log);
|
|
break;
|
|
|
|
case "RLC":
|
|
case "PDCP":
|
|
var rlcPdcpInfoMatch = RegExpInfo2.Match(log.Message);
|
|
if (rlcPdcpInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, rlcPdcpInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = rlcPdcpInfoMatch.Groups[2].Value;
|
|
}
|
|
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 };
|
|
// 根据模型类型决定 UeId 赋值
|
|
if ((Config.Model?.ToUpper() ?? "") == "MME")
|
|
{
|
|
if (coreId.HasValue) log.UeId = coreId.Value;
|
|
}
|
|
else
|
|
{
|
|
if (ranId.HasValue) log.UeId = ranId.Value;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "SIP":
|
|
var sipMatch = RegExpSIP.Match(log.Message);
|
|
if (sipMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, sipMatch.Groups[2].Value)) continue;
|
|
log.Msg0 = log.Message;
|
|
// 替换 ue_id 并调用 SetSameUe
|
|
var msg = sipMatch.Groups[3].Value;
|
|
var ueIdMatch = System.Text.RegularExpressions.Regex.Match(msg, @" ue_id=(\d+)");
|
|
if (ueIdMatch.Success)
|
|
{
|
|
if (int.TryParse(ueIdMatch.Groups[1].Value, out var ueId))
|
|
{
|
|
SetSameUe(log, ueId);
|
|
}
|
|
msg = System.Text.RegularExpressions.Regex.Replace(msg, @" ue_id=\d+", "");
|
|
}
|
|
// 拼接 from/to 和 info[1]
|
|
var dirStr = log.Direction == 1 ? " from " : " to "; // 1=UL, 2=DL
|
|
log.Message = msg + dirStr + sipMatch.Groups[1].Value;
|
|
}
|
|
break;
|
|
|
|
case "MEDIA":
|
|
case "RTP":
|
|
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 ipsecInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (ipsecInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, ipsecInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = ipsecInfoMatch.Groups[2].Value;
|
|
}
|
|
var ipsecPacketMatch = RegExpIPsec.Match(log.Message);
|
|
if (ipsecPacketMatch.Success)
|
|
{
|
|
log.IpLen = int.Parse(ipsecPacketMatch.Groups[1].Value);
|
|
HasData = true;
|
|
}
|
|
break;
|
|
|
|
case "SWU":
|
|
case "NWU":
|
|
var swuNwuInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (swuNwuInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, swuNwuInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = swuNwuInfoMatch.Groups[2].Value;
|
|
}
|
|
break;
|
|
|
|
case "IMS":
|
|
var imsInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (imsInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, imsInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = imsInfoMatch.Groups[2].Value;
|
|
|
|
var hints = ParseHints(log, new Dictionary<string, object> { { "rid", 0 }, { "aid", 0 } });
|
|
if (hints != null)
|
|
{
|
|
if (hints.ContainsKey("rid") && hints["rid"] != null)
|
|
{
|
|
SetSameUeId(log.UeId ?? 0, Convert.ToInt32(hints["rid"]));
|
|
}
|
|
if (hints.ContainsKey("aid") && hints["aid"] != null)
|
|
{
|
|
SetSameUeId(log.UeId ?? 0, Convert.ToInt32(hints["aid"]));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case "EPDG":
|
|
case "PROD":
|
|
case "COM":
|
|
var comInfoMatch = RegExpInfo1.Match(log.Message);
|
|
if (comInfoMatch.Success)
|
|
{
|
|
if (!SetLogInfo(log, comInfoMatch.Groups[1].Value)) continue;
|
|
log.Message = comInfoMatch.Groups[2].Value;
|
|
var hints = ParseHints(log);
|
|
}
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
|
|
}
|
|
this.Logs.Add(log);
|
|
// 处理提示信息
|
|
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消息
|
|
switch (log.Message.ToLower())
|
|
{
|
|
case "attach accept":
|
|
var tmsiMatch = log.FindData(RegExpNAS_TMSI);
|
|
if (tmsiMatch != null)
|
|
{
|
|
SetTmsi(log, tmsiMatch.Groups[1].Value);
|
|
}
|
|
break;
|
|
case "registration accept":
|
|
var tmsi5gMatch = log.FindData(RegExpNAS_5GTMSI);
|
|
if (tmsi5gMatch != null)
|
|
{
|
|
SetTmsi(log, tmsi5gMatch.Groups[1].Value);
|
|
}
|
|
break;
|
|
}
|
|
|
|
// 解析提示信息
|
|
var hints = ParseHints(log, new Dictionary<string, object>
|
|
{
|
|
{ "ran_ue_id", 0 },
|
|
{ "ran_address", "" }
|
|
});
|
|
|
|
// 处理RAN ID
|
|
if (hints != null && hints.TryGetValue("ran_ue_id", out var ranUeIdObj))
|
|
{
|
|
var ranUeId = Convert.ToInt32(ranUeIdObj);
|
|
if (ranUeId > 0)
|
|
{
|
|
// 获取RAN ID
|
|
string? ranId = null;
|
|
if (hints.TryGetValue("global_ran_node_id", out var globalRanNodeId))
|
|
{
|
|
ranId = globalRanNodeId.ToString();
|
|
}
|
|
else if (hints.TryGetValue("global_enb_id", out var globalEnbId))
|
|
{
|
|
ranId = globalEnbId.ToString();
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(ranId))
|
|
{
|
|
AddUERanID(log, ranUeId, ranId);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析MAC日志 - 对应JavaScript中的_logParseMac方法
|
|
/// </summary>
|
|
private void ParseMacLog(LTELog log)
|
|
{
|
|
var type = "";
|
|
var args = log.Message.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
for (int m = 0; m < args.Length; m++)
|
|
{
|
|
var param = args[m];
|
|
string? value = null;
|
|
|
|
// 检查是否有等号分隔符
|
|
var equalIndex = param.IndexOf('=');
|
|
if (equalIndex >= 0)
|
|
{
|
|
value = param.Substring(equalIndex + 1);
|
|
param = param.Substring(0, equalIndex);
|
|
}
|
|
else
|
|
{
|
|
// 检查是否有冒号分隔符
|
|
var colonIndex = param.IndexOf(':');
|
|
if (colonIndex >= 0)
|
|
{
|
|
value = param.Substring(colonIndex + 1);
|
|
param = param.Substring(0, colonIndex);
|
|
}
|
|
}
|
|
|
|
switch (param.ToLower())
|
|
{
|
|
case "ph":
|
|
if (value != null && int.TryParse(value, out var phr))
|
|
{
|
|
log.Phr = phr - 23;
|
|
}
|
|
break;
|
|
|
|
case "b":
|
|
if (value != null && int.TryParse(value, out var bValue))
|
|
{
|
|
switch (type)
|
|
{
|
|
case "SBSR":
|
|
if (bValue >= 0 && bValue < BsrTable.Length)
|
|
{
|
|
log.UlBufferSize = BsrTable[bValue];
|
|
}
|
|
break;
|
|
case "LBSR":
|
|
if (bValue >= 0 && bValue < BsrTable.Length &&
|
|
m + 3 < args.Length)
|
|
{
|
|
var b1 = int.TryParse(args[++m], out var b1Val) ? b1Val : 0;
|
|
var b2 = int.TryParse(args[++m], out var b2Val) ? b2Val : 0;
|
|
var b3 = int.TryParse(args[++m], out var b3Val) ? b3Val : 0;
|
|
|
|
var total = 0;
|
|
if (bValue < BsrTable.Length) total += BsrTable[bValue];
|
|
if (b1 < BsrTable.Length) total += BsrTable[b1];
|
|
if (b2 < BsrTable.Length) total += BsrTable[b2];
|
|
if (b3 < BsrTable.Length) total += BsrTable[b3];
|
|
|
|
log.UlBufferSize = total;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "len":
|
|
if (value != null && int.TryParse(value, out var len))
|
|
{
|
|
switch (type)
|
|
{
|
|
case "PAD":
|
|
log.MacPad = (log.MacPad ?? 0) + len;
|
|
break;
|
|
case "LCID":
|
|
log.MacLen = (log.MacLen ?? 0) + len;
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "lcid":
|
|
type = param;
|
|
break;
|
|
|
|
case "ta":
|
|
if (value != null && int.TryParse(value, out var ta))
|
|
{
|
|
log.Ta = ta - 31;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
if (value == "")
|
|
{
|
|
type = param;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取UE信息
|
|
/// </summary>
|
|
/// <param name="ueId">UE ID</param>
|
|
/// <returns>UE信息</returns>
|
|
public UEInfo? GetUE(int ueId)
|
|
{
|
|
return UeList.TryGetValue(ueId, out var ue) ? ue : null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 创建UE信息
|
|
/// </summary>
|
|
/// <param name="ueId">UE ID</param>
|
|
/// <returns>UE信息</returns>
|
|
public UEInfo CreateUE(int ueId)
|
|
{
|
|
if (!UeList.TryGetValue(ueId, out var ue))
|
|
{
|
|
ue = new UEInfo { UeId = ueId };
|
|
UeList[ueId] = ue;
|
|
}
|
|
return ue;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 根据参数获取UE
|
|
/// </summary>
|
|
/// <param name="name">参数名</param>
|
|
/// <param name="value">参数值</param>
|
|
/// <returns>UE信息</returns>
|
|
public UEInfo? GetUEByParam(string name, object value)
|
|
{
|
|
foreach (var ue in UeList.Values)
|
|
{
|
|
var property = typeof(UEInfo).GetProperty(name);
|
|
if (property?.GetValue(ue)?.Equals(value) == true)
|
|
return ue;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取UE能力信息
|
|
/// </summary>
|
|
/// <param name="ueId">UE ID</param>
|
|
/// <returns>UE能力信息</returns>
|
|
public object GetUECaps(int ueId)
|
|
{
|
|
var ue = CreateUE(ueId);
|
|
if (ue.Caps == null)
|
|
{
|
|
// 这里可以创建一个LTECaps对象
|
|
// ue.Caps = new LTECaps(ueId);
|
|
ue.Caps = new object(); // 临时实现
|
|
}
|
|
return ue.Caps;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 添加UE RAN ID
|
|
/// </summary>
|
|
/// <param name="log">日志</param>
|
|
/// <param name="ranUeId">RAN UE ID</param>
|
|
/// <param name="ranId">RAN ID</param>
|
|
private void AddUERanID(LTELog log, int ranUeId, string ranId)
|
|
{
|
|
// 简化实现,实际可能需要更复杂的逻辑
|
|
var ue = CreateUE(log.UeId ?? 0);
|
|
// 这里可以添加RAN ID相关的处理逻辑
|
|
}
|
|
|
|
/// <summary>
|
|
/// 解析提示信息
|
|
/// </summary>
|
|
/// <param name="log">日志</param>
|
|
/// <param name="config">配置</param>
|
|
/// <returns>提示信息字典</returns>
|
|
private Dictionary<string, object>? ParseHints(LTELog log, Dictionary<string, object>? config = null)
|
|
{
|
|
if (log.Info != ID_HINTS) return null;
|
|
|
|
var hints = new Dictionary<string, object>(config ?? new Dictionary<string, object>());
|
|
|
|
// 解析消息中的参数
|
|
var args = log.Message.Split(' ', StringSplitOptions.RemoveEmptyEntries);
|
|
foreach (var arg in args)
|
|
{
|
|
var parts = arg.Split('=');
|
|
if (parts.Length == 2)
|
|
{
|
|
hints[parts[0]] = parts[1];
|
|
}
|
|
}
|
|
|
|
// 处理IMEI/IMEISV
|
|
if (hints.TryGetValue("imei", out var imeiObj))
|
|
{
|
|
var imei = imeiObj.ToString();
|
|
var ue = GetUEByParam("Imei", imei);
|
|
if (ue != null)
|
|
{
|
|
SetSameUe(log, ue.UeId);
|
|
}
|
|
else
|
|
{
|
|
ue = CreateUE(log.UeId ?? 0);
|
|
ue.Imei = imei;
|
|
}
|
|
}
|
|
else if (hints.TryGetValue("imeisv", out var imeisvObj))
|
|
{
|
|
var imeisv = imeisvObj.ToString();
|
|
var imei = imeisv.Length > 2 ? imeisv[..^2] : imeisv; // 移除最后两个字符
|
|
var ue = GetUEByParam("Imei", imei);
|
|
if (ue != null)
|
|
{
|
|
SetSameUe(log, ue.UeId);
|
|
}
|
|
else
|
|
{
|
|
ue = CreateUE(log.UeId ?? 0);
|
|
ue.Imei = imei;
|
|
}
|
|
}
|
|
|
|
// 处理IMSI
|
|
if (hints.TryGetValue("imsi", out var imsiObj))
|
|
{
|
|
var imsi = imsiObj.ToString();
|
|
var ue = GetUEByParam("Imsi", imsi);
|
|
if (ue == null)
|
|
{
|
|
ue = CreateUE(log.UeId ?? 0);
|
|
ue.Imsi = imsi;
|
|
}
|
|
}
|
|
|
|
return hints;
|
|
}
|
|
|
|
#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;
|
|
|
|
PDSCH = _lteLogs.String2Id("PDSCH");
|
|
PDCCH = _lteLogs.String2Id("PDCCH");
|
|
EPDCCH = _lteLogs.String2Id("EPDCCH");
|
|
PUSCH = _lteLogs.String2Id("PUSCH");
|
|
PUCCH = _lteLogs.String2Id("PUCCH");
|
|
NPDSCH = _lteLogs.String2Id("NPDSCH");
|
|
NPUSCH = _lteLogs.String2Id("NPUSCH");
|
|
NPBCH = _lteLogs.String2Id("NPBCH");
|
|
BCCH_NR = _lteLogs.String2Id("BCCH-NR");
|
|
ID_HINTS = _lteLogs.String2Id("HINTS");
|
|
SRS = _lteLogs.String2Id("SRS");
|
|
CSI = _lteLogs.String2Id("CSI");
|
|
SIM_EVENT = _lteLogs.String2Id("SIM-Event");
|
|
IP_SIM = _lteLogs.String2Id("IP-SIM");
|
|
|
|
// 注意:LogsManager 的字符串ID缓存是全局的,不需要重置
|
|
// 这样可以保持字符串ID的一致性
|
|
}
|
|
|
|
/// <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
|
|
|
|
#region 模型管理方法
|
|
|
|
/// <summary>
|
|
/// 模型提示信息
|
|
/// </summary>
|
|
private string? _modelHint;
|
|
|
|
/// <summary>
|
|
/// 传输方向
|
|
/// </summary>
|
|
public int TxDir { get; set; }
|
|
|
|
/// <summary>
|
|
/// 接收方向
|
|
/// </summary>
|
|
public int RxDir { get; set; }
|
|
|
|
/// <summary>
|
|
/// RAN ID列表
|
|
/// </summary>
|
|
public List<string> RanIds { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 层方向配置
|
|
/// </summary>
|
|
private Dictionary<string, Dictionary<string, int>> _layerDir = new();
|
|
|
|
/// <summary>
|
|
/// 尝试模型提示
|
|
/// </summary>
|
|
/// <param name="models">模型列表</param>
|
|
/// <returns>是否成功设置模型</returns>
|
|
public bool TryModelHint(List<string> models)
|
|
{
|
|
if (string.IsNullOrEmpty(_modelHint))
|
|
return false;
|
|
|
|
foreach (var model in models)
|
|
{
|
|
if (_clientModelConfigs.TryGetValue(model, out var config))
|
|
{
|
|
var modelHint = config.ModelHint;
|
|
if (string.IsNullOrEmpty(modelHint))
|
|
{
|
|
// 没有提示,基于模型名称
|
|
modelHint = model;
|
|
}
|
|
|
|
if (Regex.IsMatch(_modelHint, modelHint, RegexOptions.IgnoreCase))
|
|
{
|
|
return SetModel(model);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置模型
|
|
/// </summary>
|
|
/// <param name="model">模型名称</param>
|
|
/// <param name="force">是否强制设置</param>
|
|
/// <returns>是否成功设置</returns>
|
|
public bool SetModel(string model, bool force = false)
|
|
{
|
|
// 如果模型相同且不强制,直接返回成功
|
|
if (model == Config.Model && !force)
|
|
return true;
|
|
|
|
Config.Model = model;
|
|
|
|
// 根据模型设置传输方向
|
|
switch (model?.ToUpper())
|
|
{
|
|
case "ENB":
|
|
TxDir = 2; // DIR_DL
|
|
RxDir = 1; // DIR_UL
|
|
RanIds.Clear();
|
|
break;
|
|
case "UE":
|
|
TxDir = 1; // DIR_UL
|
|
RxDir = 2; // DIR_DL
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
// 设置层方向配置
|
|
if (_clientModelConfigs.TryGetValue(model ?? "", out var config))
|
|
{
|
|
_layerDir = config.LayerDir ?? new Dictionary<string, Dictionary<string, int>>();
|
|
}
|
|
else
|
|
{
|
|
_layerDir.Clear();
|
|
}
|
|
|
|
// 初始化层方向配置
|
|
InitializeLayerDirectionConfig(model);
|
|
|
|
// 通知日志管理器更新网格列
|
|
_lteLogs.GridColumnsUpdate();
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置模型提示
|
|
/// </summary>
|
|
/// <param name="hint">提示信息</param>
|
|
public void SetModelHint(string hint)
|
|
{
|
|
_modelHint = hint;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 初始化层方向配置 - 对应JavaScript版本的层方向配置初始化
|
|
/// </summary>
|
|
/// <param name="model">模型名称</param>
|
|
private void InitializeLayerDirectionConfig(string? model)
|
|
{
|
|
// 获取层配置
|
|
var layerConfig = _lteLogs.GetLayerConfig();
|
|
|
|
// 为每个层初始化默认方向配置
|
|
foreach (var layer in layerConfig.Keys)
|
|
{
|
|
if (!_layerDir.ContainsKey(layer))
|
|
{
|
|
_layerDir[layer] = new Dictionary<string, int>
|
|
{
|
|
{ "UL", LogsManager.DIR_UL },
|
|
{ "DL", LogsManager.DIR_DL },
|
|
{ "FROM", LogsManager.DIR_DL },
|
|
{ "TO", LogsManager.DIR_UL },
|
|
{ "-", LogsManager.DIR_NONE }
|
|
};
|
|
}
|
|
}
|
|
|
|
// 根据模型设置特殊的方向配置(与 JavaScript 版本保持一致)
|
|
switch (model?.ToUpper())
|
|
{
|
|
case "MME":
|
|
// MME 模型特殊配置
|
|
foreach (var layer in layerConfig.Keys)
|
|
{
|
|
if (layer != "IMS" && layer != "CX" && layer != "RX" &&
|
|
layer != "S6" && layer != "S13" && layer != "SGsAP" &&
|
|
layer != "SBcAP" && layer != "N8" && layer != "N12" &&
|
|
layer != "N13" && layer != "N17" && layer != "N50" &&
|
|
layer != "MMS" && layer != "HTTP2" && layer != "LCSAP" &&
|
|
layer != "NL1" && layer != "GTPC")
|
|
{
|
|
if (_layerDir.ContainsKey(layer))
|
|
{
|
|
_layerDir[layer]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir[layer]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "IMS":
|
|
// IMS 模型特殊配置
|
|
foreach (var layer in layerConfig.Keys)
|
|
{
|
|
if (_layerDir.ContainsKey(layer))
|
|
{
|
|
_layerDir[layer]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir[layer]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case "N3IWF":
|
|
// N3IWF 模型特殊配置
|
|
if (_layerDir.ContainsKey("IKEV2"))
|
|
{
|
|
_layerDir["IKEV2"]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir["IKEV2"]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
if (_layerDir.ContainsKey("IPSEC"))
|
|
{
|
|
_layerDir["IPSEC"]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir["IPSEC"]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
break;
|
|
|
|
case "ENB":
|
|
// ENB 模型特殊配置
|
|
if (_layerDir.ContainsKey("TRX"))
|
|
{
|
|
_layerDir["TRX"]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir["TRX"]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
break;
|
|
|
|
case "UE":
|
|
// UE 模型特殊配置
|
|
if (_layerDir.ContainsKey("GTPU"))
|
|
{
|
|
_layerDir["GTPU"]["FROM"] = LogsManager.DIR_UL;
|
|
_layerDir["GTPU"]["TO"] = LogsManager.DIR_DL;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region 模型配置
|
|
|
|
/// <summary>
|
|
/// 客户端模型配置类
|
|
/// </summary>
|
|
public class ClientModelConfig
|
|
{
|
|
public string? Icon { get; set; }
|
|
public string? ModelHint { get; set; }
|
|
public string? Title { get; set; }
|
|
public Dictionary<string, Dictionary<string, int>>? LayerDir { get; set; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// 客户端模型配置字典
|
|
/// </summary>
|
|
private static readonly Dictionary<string, ClientModelConfig> _clientModelConfigs = new()
|
|
{
|
|
{ "RUE", new ClientModelConfig { Icon = "icon-ue2" } },
|
|
{ "UE", new ClientModelConfig { Icon = "icon-ue" } },
|
|
{ "PROBE", new ClientModelConfig() },
|
|
{ "ENB", new ClientModelConfig { Icon = "icon-air", ModelHint = "enb|gnb|bbu", Title = "RAN" } },
|
|
{ "N3IWF", new ClientModelConfig { Icon = "icon-air", ModelHint = "n3iwf", Title = "N3IWF" } },
|
|
{ "MME", new ClientModelConfig { Icon = "icon-server", ModelHint = "epc|mme|amf", Title = "CN" } },
|
|
{ "MBMSGW", new ClientModelConfig { Icon = "icon-download", ModelHint = "mbms" } },
|
|
{ "IMS", new ClientModelConfig { Icon = "icon-dial" } },
|
|
{ "MONITOR", new ClientModelConfig { Icon = "icon-monitor" } },
|
|
{ "LICENSE", new ClientModelConfig { Icon = "icon-file" } }
|
|
};
|
|
|
|
#endregion
|
|
|
|
/// <summary>
|
|
/// 设置两个UE ID为同一UE(合并UE信息)
|
|
/// </summary>
|
|
private void SetSameUeId(int id0, int id1)
|
|
{
|
|
if (id0 == id1) return;
|
|
var ue0 = UeList.ContainsKey(id0) ? UeList[id0] : null;
|
|
var ue1 = UeList.ContainsKey(id1) ? UeList[id1] : null;
|
|
|
|
if (ue0 == ue1)
|
|
{
|
|
if (ue1 == null)
|
|
{
|
|
ue1 = CreateUE(id1);
|
|
UeList[id0] = ue1;
|
|
}
|
|
}
|
|
else if (ue0 == null)
|
|
{
|
|
UeList[id0] = ue1;
|
|
}
|
|
else if (ue1 == null)
|
|
{
|
|
UeList[id1] = ue0;
|
|
}
|
|
else
|
|
{
|
|
// 合并所有指向id1的UE为ue0
|
|
var keysToUpdate = UeList.Where(kv => kv.Value == ue1).Select(kv => kv.Key).ToList();
|
|
foreach (var key in keysToUpdate)
|
|
{
|
|
int newKey =(int)key;
|
|
UeList[newKey] = ue0;
|
|
}
|
|
// 合并属性
|
|
if (string.IsNullOrEmpty(ue0.Imsi)) ue0.Imsi = ue1.Imsi;
|
|
if (string.IsNullOrEmpty(ue0.Imei)) ue0.Imei = ue1.Imei;
|
|
if (ue0.Caps == null) ue0.Caps = ue1.Caps;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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>
|
|
/// 模式(ran/ims)
|
|
/// </summary>
|
|
public string Mode { get; set; } = "ran";
|
|
|
|
/// <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; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// LTE小区类 - 对应JavaScript的LTECell
|
|
/// </summary>
|
|
public class LTECell
|
|
{
|
|
/// <summary>
|
|
/// 小区ID
|
|
/// </summary>
|
|
public int CellId { get; set; }
|
|
|
|
/// <summary>
|
|
/// 无线接入技术
|
|
/// </summary>
|
|
public string Rat { get; set; } = "lte";
|
|
|
|
/// <summary>
|
|
/// 下行资源块数量
|
|
/// </summary>
|
|
public int NRbDl { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// 上行资源块数量
|
|
/// </summary>
|
|
public int NRbUl { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// 下行多用户
|
|
/// </summary>
|
|
public int DlMu { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// 上行多用户
|
|
/// </summary>
|
|
public int UlMu { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// 模式
|
|
/// </summary>
|
|
public string Mode { get; set; } = "FDD";
|
|
|
|
/// <summary>
|
|
/// 物理小区ID
|
|
/// </summary>
|
|
public string Pci { get; set; } = "?";
|
|
|
|
/// <summary>
|
|
/// 循环前缀
|
|
/// </summary>
|
|
public int CyclicPrefix { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// PUCCH偏移
|
|
/// </summary>
|
|
public int DeltaPucchShift { get; set; } = 2;
|
|
|
|
/// <summary>
|
|
/// CQI资源块数量
|
|
/// </summary>
|
|
public int NRbCqi { get; set; } = 0;
|
|
|
|
/// <summary>
|
|
/// 下行子帧位图
|
|
/// </summary>
|
|
public string? BrDlSfBitmap { get; set; }
|
|
|
|
/// <summary>
|
|
/// NB下行子帧位图
|
|
/// </summary>
|
|
public string? NbDlSfBitmap { get; set; }
|
|
|
|
/// <summary>
|
|
/// PRACH频率偏移
|
|
/// </summary>
|
|
public int PrachFreqOffset { get; set; } = 2;
|
|
|
|
/// <summary>
|
|
/// PRACH配置索引
|
|
/// </summary>
|
|
public int PrachConfigIndex { get; set; } = 2;
|
|
|
|
/// <summary>
|
|
/// 标签
|
|
/// </summary>
|
|
public string? Label { get; set; }
|
|
|
|
/// <summary>
|
|
/// SIB1是否已解码
|
|
/// </summary>
|
|
public bool Sib1Decoded { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// SIB2是否已解码
|
|
/// </summary>
|
|
public bool Sib2Decoded { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// 其他参数
|
|
/// </summary>
|
|
public Dictionary<string, object> Parameters { get; set; } = new();
|
|
|
|
/// <summary>
|
|
/// 构造函数
|
|
/// </summary>
|
|
/// <param name="cellId">小区ID</param>
|
|
/// <param name="config">配置字典</param>
|
|
public LTECell(int cellId, Dictionary<string, object> config)
|
|
{
|
|
CellId = cellId;
|
|
|
|
// 从配置字典设置属性
|
|
foreach (var kvp in config)
|
|
{
|
|
SetProperty(kvp.Key, kvp.Value);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置属性
|
|
/// </summary>
|
|
/// <param name="name">属性名</param>
|
|
/// <param name="value">属性值</param>
|
|
private void SetProperty(string name, object value)
|
|
{
|
|
switch (name)
|
|
{
|
|
case "cell_id":
|
|
CellId = Convert.ToInt32(value);
|
|
break;
|
|
case "rat":
|
|
Rat = value.ToString() ?? "lte";
|
|
break;
|
|
case "n_rb_dl":
|
|
NRbDl = Convert.ToInt32(value);
|
|
break;
|
|
case "n_rb_ul":
|
|
NRbUl = Convert.ToInt32(value);
|
|
break;
|
|
case "dl_mu":
|
|
DlMu = Convert.ToInt32(value);
|
|
break;
|
|
case "ul_mu":
|
|
UlMu = Convert.ToInt32(value);
|
|
break;
|
|
case "mode":
|
|
Mode = value.ToString() ?? "FDD";
|
|
break;
|
|
case "pci":
|
|
Pci = value.ToString() ?? "?";
|
|
break;
|
|
case "cyclic_prefix":
|
|
CyclicPrefix = Convert.ToInt32(value);
|
|
break;
|
|
case "delta_pucch_shift":
|
|
DeltaPucchShift = Convert.ToInt32(value);
|
|
break;
|
|
case "n_rb_cqi":
|
|
NRbCqi = Convert.ToInt32(value);
|
|
break;
|
|
case "br_dl_sf_bitmap":
|
|
BrDlSfBitmap = value.ToString();
|
|
break;
|
|
case "nb_dl_sf_bitmap":
|
|
NbDlSfBitmap = value.ToString();
|
|
break;
|
|
case "prach_freq_offset":
|
|
PrachFreqOffset = Convert.ToInt32(value);
|
|
break;
|
|
case "prach_config_index":
|
|
PrachConfigIndex = Convert.ToInt32(value);
|
|
break;
|
|
case "label":
|
|
Label = value.ToString();
|
|
break;
|
|
case "sib1Decoded":
|
|
Sib1Decoded = Convert.ToBoolean(value);
|
|
break;
|
|
case "sib2Decoded":
|
|
Sib2Decoded = Convert.ToBoolean(value);
|
|
break;
|
|
default:
|
|
// 其他参数存储在Parameters字典中
|
|
Parameters[name] = value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 设置RAT类型
|
|
/// </summary>
|
|
/// <param name="rat">RAT类型</param>
|
|
/// <param name="force">是否强制设置</param>
|
|
public void SetRat(string rat, bool force = false)
|
|
{
|
|
if (rat == Rat && !force) return;
|
|
|
|
switch (rat.ToLower())
|
|
{
|
|
case "nbiot":
|
|
NRbDl = 2;
|
|
NRbUl = 12;
|
|
break;
|
|
}
|
|
Rat = rat;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取时隙数量
|
|
/// </summary>
|
|
/// <param name="ul">是否为上行</param>
|
|
/// <returns>时隙数量</returns>
|
|
public int GetSlotCount(bool ul)
|
|
{
|
|
return 10 * (1 << (ul ? UlMu : DlMu));
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取下行比例
|
|
/// </summary>
|
|
/// <returns>下行比例</returns>
|
|
public double GetDLRatio()
|
|
{
|
|
if (Mode != "TDD") return 1;
|
|
|
|
var ratios = new[] { 4.0 / 10, 6.0 / 10, 8.0 / 10, 7.0 / 10, 8.0 / 10, 9.0 / 10, 5.0 / 10 };
|
|
if (Parameters.TryGetValue("uldl_config", out var config) && config is int uldlConfig)
|
|
{
|
|
return uldlConfig < ratios.Length ? ratios[uldlConfig] : 1;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取小区名称
|
|
/// </summary>
|
|
/// <returns>小区名称</returns>
|
|
public string GetName()
|
|
{
|
|
return !string.IsNullOrEmpty(Label) ? Label : $"#{CellId}";
|
|
}
|
|
}
|