rewrite udp server handler, change router and protocol
This commit is contained in:
parent
5fa4860bfb
commit
ae34cf6436
|
@ -79,13 +79,16 @@ try
|
|||
|
||||
// Setup Program
|
||||
MsgBus.Init();
|
||||
// if (app.Environment.IsDevelopment()) MsgBus.UDPServer.EnableDebugMode = true;
|
||||
MsgBus.UDPServer.EnableDebugMode = true;
|
||||
|
||||
// Router
|
||||
// API Get
|
||||
app.MapGet("/", () => Results.Redirect("/swagger"));
|
||||
app.MapGet("/api/GetRecvDataArray", Router.API.GetRecvDataArray);
|
||||
// API Post
|
||||
app.MapPost("/api/SendString", Router.API.SendString);
|
||||
// API Put
|
||||
app.MapPost("/api/SendBytes", Router.API.SendBytes);
|
||||
app.MapPost("/api/SendAddrPackage", Router.API.SendAddrPackage);
|
||||
app.MapPost("/api/SendDataPackage", Router.API.SendDataPackage);
|
||||
// API Jtag
|
||||
|
|
|
@ -64,7 +64,7 @@ namespace Common
|
|||
return num;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public static Result<byte[]> MultiBitsToBytes(ulong bits1, uint bits1Len, ulong bits2, uint bits2Len)
|
||||
{
|
||||
|
@ -99,7 +99,7 @@ namespace Common
|
|||
}
|
||||
|
||||
|
||||
public static Result<byte[]> StringToBytes(string str, int numBase = 16)
|
||||
public static byte[] StringToBytes(string str, int numBase = 16)
|
||||
{
|
||||
var len = str.Length;
|
||||
var bytesLen = len / 2;
|
||||
|
|
|
@ -39,19 +39,41 @@ namespace Router
|
|||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
private const string LOCALHOST = "127.0.0.1";
|
||||
|
||||
/// <summary>
|
||||
/// 发送字符串
|
||||
/// </summary>
|
||||
/// <param name="address">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port">设备端口号</param>
|
||||
/// <param name="text">Text for send</param>
|
||||
/// <returns>Json: true or false</returns>
|
||||
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port" example="1234">设备端口号</param>
|
||||
/// <param name="text" example="Hello Server!">Text for send</param>
|
||||
[HttpPost("{address}/{port}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> SendString(string address, int port, string text)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendStringAsync(endPoint, [text]);
|
||||
if (ret) { return Results.Json(new Response() { IsSuccess = true }); }
|
||||
else { return Results.Json(new Response() { IsSuccess = false }); }
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送二进制数据
|
||||
/// </summary>
|
||||
/// <param name="address" example="127.0.0.1">IPV4 或者 IPV6 地址</param>
|
||||
/// <param name="port" example="1234">设备端口号</param>
|
||||
/// <param name="bytes" example="FFFFAAAA">16进制文本</param>
|
||||
[HttpPost("{address}/{port}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> SendBytes(string address, int port, string bytes)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendBytesAsync(endPoint, NumberProcessor.StringToBytes(bytes));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
|
||||
|
@ -62,8 +84,8 @@ namespace Router
|
|||
/// <param name="port" example="1234">UDP 端口号</param>
|
||||
/// <param name="opts">地址包选项</param>
|
||||
[HttpPost("{address}/{port}")]
|
||||
[ProducesResponseType(200)]
|
||||
[ProducesResponseType(500)]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> SendAddrPackage(
|
||||
string address,
|
||||
int port,
|
||||
|
@ -71,20 +93,27 @@ namespace Router
|
|||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendAddrPackAsync(endPoint, new WebProtocol.SendAddrPackage(opts));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
[HttpPost("{address}/{port}")]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> SendDataPackage(string address, int port, string data)
|
||||
{
|
||||
var endPoint = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
var ret = await UDPClientPool.SendDataPackAsync(endPoint,
|
||||
new WebProtocol.SendDataPackage(NumberProcessor.StringToBytes(data).Value));
|
||||
if (ret) { return Results.Json(new Response() { IsSuccess = true }); }
|
||||
else { return Results.Json(new Response() { IsSuccess = false }); }
|
||||
new WebProtocol.SendDataPackage(NumberProcessor.StringToBytes(data)));
|
||||
|
||||
if (ret) { return TypedResults.Ok(); }
|
||||
else { return TypedResults.InternalServerError(); }
|
||||
}
|
||||
|
||||
[HttpGet("{address}")]
|
||||
[ProducesResponseType(typeof(List<UDPData>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> GetRecvDataArray(string address)
|
||||
{
|
||||
var ret = await MsgBus.UDPServer.GetDataArrayAsync(address);
|
||||
|
@ -94,26 +123,19 @@ namespace Router
|
|||
var dataJson = JsonConvert.SerializeObject(ret.Value);
|
||||
logger.Debug($"Get Receive Successfully: {dataJson}");
|
||||
|
||||
return TypedResults.Ok(new Response
|
||||
{
|
||||
IsSuccess = true,
|
||||
Data = ret.Value,
|
||||
});
|
||||
return TypedResults.Ok(ret.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Debug("Get Receive Failed");
|
||||
return TypedResults.Json(new Response
|
||||
{
|
||||
IsSuccess = false,
|
||||
Data = ""
|
||||
});
|
||||
return TypedResults.InternalServerError();
|
||||
}
|
||||
}
|
||||
|
||||
public class Jtag
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
/// <summary>
|
||||
/// 执行一个Jtag命令
|
||||
/// </summary>
|
||||
|
@ -121,16 +143,16 @@ namespace Router
|
|||
/// <param name="port"> 设备端口 </param>
|
||||
/// <param name="hexDevAddr"> 16进制设备目的地址(Jtag) </param>
|
||||
/// <param name="hexCmd"> 16进制命令 </param>
|
||||
/// <param name="hexExRet"> 16进制预期结果 </param>
|
||||
/// <returns> Response </returns>
|
||||
public static async ValueTask<IResult> RunCommand(string address, int port, string hexDevAddr, string hexCmd, string hexExRet)
|
||||
[HttpPost]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> RunCommand(string address, int port, string hexDevAddr, string hexCmd)
|
||||
{
|
||||
var jtagCtrl = new JtagController.Jtag(address, port);
|
||||
var ret = await jtagCtrl.RunCommand(Convert.ToUInt32(hexDevAddr, 16), Convert.ToUInt32(hexCmd, 16), Convert.ToUInt32(hexExRet, 16));
|
||||
var ret = await jtagCtrl.RunCommand(Convert.ToUInt32(hexDevAddr, 16), Convert.ToUInt32(hexCmd, 16));
|
||||
|
||||
|
||||
if (ret.IsSuccessful) { return Results.Json(new Response() { IsSuccess = true }); }
|
||||
else { return Results.Json(new Response() { IsSuccess = false, Data = ret.Error }); }
|
||||
if (ret.IsSuccessful) { return TypedResults.Ok(ret.Value); }
|
||||
else { return TypedResults.InternalServerError(ret.Error); }
|
||||
}
|
||||
|
||||
|
||||
|
@ -139,14 +161,16 @@ namespace Router
|
|||
/// </summary>
|
||||
/// <param name="address"> 设备地址 </param>
|
||||
/// <param name="port"> 设备端口 </param>
|
||||
/// <returns> Response </returns>
|
||||
[HttpGet]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public static async ValueTask<IResult> GetDeviceIDCode(string address, int port)
|
||||
{
|
||||
var jtagCtrl = new JtagController.Jtag(address, port);
|
||||
var ret = await jtagCtrl.ReadIDCode();
|
||||
|
||||
if (ret.IsSuccessful) { return Results.Json(new Response() { IsSuccess = true, Data = ret.Value }); }
|
||||
else { return Results.Json(new Response() { IsSuccess = false, Data = ret.Error }); }
|
||||
if (ret.IsSuccessful) { return TypedResults.Ok(ret.Value); }
|
||||
else { return TypedResults.InternalServerError(ret.Error); }
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
using System.Net;
|
||||
using System.Net.Sockets;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using DotNext;
|
||||
using DotNext.Threading;
|
||||
using Newtonsoft.Json;
|
||||
|
@ -42,6 +43,9 @@ public class UDPData
|
|||
/// <summary>
|
||||
/// UDP Server
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// UDP 服务器
|
||||
/// </summary>
|
||||
public class UDPServer
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
@ -52,6 +56,11 @@ public class UDPServer
|
|||
private Dictionary<string, List<UDPData>> udpData = new Dictionary<string, List<UDPData>>();
|
||||
private AsyncReaderWriterLock udpDataLock = new AsyncReaderWriterLock(1);
|
||||
|
||||
/// <summary>
|
||||
/// 是否开启Debug模式
|
||||
/// </summary>
|
||||
public bool EnableDebugMode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Construct a udp server with fixed port
|
||||
/// </summary>
|
||||
|
@ -256,52 +265,133 @@ public class UDPServer
|
|||
{
|
||||
var remoteEP = new IPEndPoint(IPAddress.Any, listenPort);
|
||||
byte[] bytes = listener.EndReceive(res, ref remoteEP);
|
||||
var nowtime = DateTime.Now;
|
||||
|
||||
// Handle RemoteEP
|
||||
string remoteStr = "Unknown";
|
||||
using (udpDataLock.AcquireWriteLock())
|
||||
if (remoteEP is null)
|
||||
{
|
||||
if (remoteEP is not null)
|
||||
{
|
||||
var remoteAddress = remoteEP.Address.ToString();
|
||||
var remotePort = remoteEP.Port;
|
||||
// Record UDP Receive Data
|
||||
if (udpData.ContainsKey(remoteAddress))
|
||||
{
|
||||
var listData = udpData[remoteAddress];
|
||||
listData.Add(new UDPData()
|
||||
{
|
||||
Address = remoteAddress,
|
||||
Port = remotePort,
|
||||
Data = bytes,
|
||||
DateTime = nowtime,
|
||||
HasRead = false,
|
||||
});
|
||||
logger.Trace("Receive data from old client");
|
||||
}
|
||||
else
|
||||
{
|
||||
udpData.Add(remoteAddress, new List<UDPData>([new UDPData()
|
||||
{
|
||||
Address = remoteAddress,
|
||||
Port = remotePort,
|
||||
Data = bytes,
|
||||
DateTime = nowtime,
|
||||
HasRead = false,
|
||||
}]));
|
||||
logger.Trace("Receive data from new client");
|
||||
}
|
||||
logger.Debug($"Receive Data from Unknown at {DateTime.Now.ToString()}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
goto BEGIN_RECEIVE;
|
||||
}
|
||||
|
||||
remoteStr = $"{remoteAddress}:{remotePort}";
|
||||
|
||||
// If enable Debug mode, exec Debug handler
|
||||
if (EnableDebugMode)
|
||||
{
|
||||
DebugHandler(new IPEndPoint(IPAddress.Parse("127.0.0.1"), this.listenPort), bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle Package
|
||||
PrintData(RecordUDPData(bytes, remoteEP));
|
||||
}
|
||||
|
||||
BEGIN_RECEIVE:
|
||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
}
|
||||
|
||||
private void DebugHandler(IPEndPoint ep, byte[] bytes)
|
||||
{
|
||||
// Print Receive Data
|
||||
var data = new UDPData()
|
||||
{
|
||||
Address = ep.Address.ToString(),
|
||||
Port = ep.Port,
|
||||
Data = bytes,
|
||||
DateTime = DateTime.Now,
|
||||
HasRead = false,
|
||||
};
|
||||
PrintData(data);
|
||||
|
||||
// Handle Pack Type
|
||||
var sign = bytes[0];
|
||||
if (sign == (byte)WebProtocol.PackSign.SendAddr)
|
||||
{
|
||||
var resData = WebProtocol.SendAddrPackage.FromBytes(bytes);
|
||||
if (!resData.IsSuccessful)
|
||||
{
|
||||
logger.Warn("DebugHandler: Convert to SendAddrPackage failed");
|
||||
return;
|
||||
}
|
||||
|
||||
if (resData.Value.Options.IsWrite)
|
||||
{
|
||||
var pack = new WebProtocol.RecvRespPackage(resData.Value.Options.CommandID, true);
|
||||
SendBytes(ep, pack.ToBytes());
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Warn("Receive data from unknown client");
|
||||
var pack = new WebProtocol.RecvDataPackage(resData.Value.Options.CommandID, true, [0, 0, 0, 0]);
|
||||
SendBytes(ep, pack.ToBytes());
|
||||
}
|
||||
}
|
||||
else if (sign == (byte)WebProtocol.PackSign.SendData)
|
||||
{
|
||||
}
|
||||
else
|
||||
{
|
||||
SendString(ep, "DebugHandler: Receive Data");
|
||||
}
|
||||
|
||||
// Handle Package
|
||||
logger.Trace("DebugHandler: send pack successfully");
|
||||
}
|
||||
|
||||
private bool SendBytes(IPEndPoint endPoint, byte[] buf)
|
||||
{
|
||||
var sendLen = listener.Send(buf, endPoint);
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
private bool SendString(IPEndPoint endPoint, string text)
|
||||
{
|
||||
byte[] buf = Encoding.ASCII.GetBytes(text);
|
||||
var sendLen = listener.Send(buf, endPoint);
|
||||
|
||||
if (sendLen == buf.Length) { return true; }
|
||||
else { return false; }
|
||||
}
|
||||
|
||||
private UDPData RecordUDPData(byte[] bytes, IPEndPoint remoteEP)
|
||||
{
|
||||
using (udpDataLock.AcquireWriteLock())
|
||||
{
|
||||
var remoteAddress = remoteEP.Address.ToString();
|
||||
var remotePort = remoteEP.Port;
|
||||
var data = new UDPData()
|
||||
{
|
||||
Address = remoteAddress,
|
||||
Port = remotePort,
|
||||
Data = bytes,
|
||||
DateTime = DateTime.Now,
|
||||
HasRead = false,
|
||||
};
|
||||
|
||||
// Record UDP Receive Data
|
||||
if (udpData.ContainsKey(remoteAddress))
|
||||
{
|
||||
var listData = udpData[remoteAddress];
|
||||
listData.Add(data);
|
||||
logger.Trace("Receive data from old client");
|
||||
}
|
||||
else
|
||||
{
|
||||
udpData.Add(remoteAddress, new List<UDPData>([data]));
|
||||
logger.Trace("Receive data from new client");
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 输出UDP Data到log中
|
||||
/// </summary>
|
||||
/// <param name="data">UDP数据</param>
|
||||
public void PrintData(UDPData data)
|
||||
{
|
||||
var bytes = data.Data;
|
||||
var sign = bytes[0];
|
||||
string recvData = "";
|
||||
if (sign == (byte)WebProtocol.PackSign.SendAddr)
|
||||
|
@ -334,20 +424,16 @@ public class UDPServer
|
|||
recvData = Encoding.ASCII.GetString(bytes, 0, bytes.Length);
|
||||
}
|
||||
|
||||
logger.Debug($"Receive Data from {remoteStr} at {nowtime.ToString()}:");
|
||||
logger.Debug($"Original Data: {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
if (recvData.Length != 0) logger.Debug(recvData);
|
||||
// RecordAllData();
|
||||
|
||||
|
||||
listener.BeginReceive(new AsyncCallback(ReceiveHandler), null);
|
||||
logger.Debug($"Receive Data from {data.Address}:{data.Port} at {data.DateTime.ToString()}:");
|
||||
logger.Debug($" Original Data : {BitConverter.ToString(bytes).Replace("-", " ")}");
|
||||
if (recvData.Length != 0) logger.Debug($" Decoded Data : {recvData}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将所有数据输出到log中
|
||||
/// </summary>
|
||||
/// <returns> void </returns>
|
||||
public void RecordAllData()
|
||||
public void PrintAllData()
|
||||
{
|
||||
using (udpDataLock.AcquireReadLock())
|
||||
{
|
||||
|
|
|
@ -172,6 +172,24 @@ namespace WebProtocol
|
|||
this.address = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取对应地址包选项
|
||||
/// </summary>
|
||||
public SendAddrPackOptions Options
|
||||
{
|
||||
get
|
||||
{
|
||||
return new SendAddrPackOptions()
|
||||
{
|
||||
Address = this.address,
|
||||
BurstLength = this.burstLength,
|
||||
BurstType = (BurstType)(this.commandType >> 6),
|
||||
CommandID = Convert.ToByte((this.commandType >> 4) & 0b11),
|
||||
IsWrite = Convert.ToBoolean(this.commandType & 1)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将地址包转化为字节数组
|
||||
/// </summary>
|
||||
|
@ -298,6 +316,31 @@ namespace WebProtocol
|
|||
_ = this._reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FPGA->Server 读响应包
|
||||
/// 构造函数
|
||||
/// </summary>
|
||||
/// <param name="commandID"> 任务ID号 </param>
|
||||
/// <param name="isSuccess">是否读取成功</param>
|
||||
/// <param name="bodyData"> 数据 </param>
|
||||
public RecvDataPackage(byte commandID, bool isSuccess, byte[] bodyData)
|
||||
{
|
||||
this.commandID = commandID;
|
||||
this.resp = Convert.ToByte(isSuccess);
|
||||
this.bodyData = bodyData;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过接受包选项构建读响应包
|
||||
/// </summary>
|
||||
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
|
||||
public RecvDataPackage(RecvPackOptions opts)
|
||||
{
|
||||
this.commandID = opts.CommandID;
|
||||
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
|
||||
this.bodyData = opts.Data ?? (byte[])[0, 0, 0, 0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取读响应包选项
|
||||
/// </summary>
|
||||
|
@ -337,6 +380,25 @@ namespace WebProtocol
|
|||
);
|
||||
return new RecvDataPackage(bytes[1], bytes[2], bytes[4..]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将数据包转化为字节数组
|
||||
/// </summary>
|
||||
/// <returns>字节数组</returns>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var bodyDataLen = bodyData.Length;
|
||||
var arr = new byte[4 + bodyDataLen];
|
||||
|
||||
arr[0] = this.sign;
|
||||
arr[1] = this.commandID;
|
||||
arr[2] = this.resp;
|
||||
|
||||
Array.Copy(bodyData, 0, arr, 4, bodyDataLen);
|
||||
|
||||
return arr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary> 写响应包 </summary>
|
||||
|
@ -361,6 +423,27 @@ namespace WebProtocol
|
|||
_ = this._reserved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 构建写响应包
|
||||
/// </summary>
|
||||
/// <param name="commandID">任务ID</param>
|
||||
/// <param name="isSuccess">是否写成功</param>
|
||||
public RecvRespPackage(byte commandID, bool isSuccess)
|
||||
{
|
||||
this.commandID = commandID;
|
||||
this.resp = Convert.ToByte(isSuccess);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过接受包选项构建写响应包
|
||||
/// </summary>
|
||||
/// <param name="opts">接收包(读响应包和写响应包)选项</param>
|
||||
public RecvRespPackage(RecvPackOptions opts)
|
||||
{
|
||||
this.commandID = opts.CommandID;
|
||||
this.resp = Convert.ToByte(opts.IsSuccess ? 0b10 : 0b00);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取写响应包选项
|
||||
/// </summary>
|
||||
|
@ -400,5 +483,20 @@ namespace WebProtocol
|
|||
);
|
||||
return new RecvRespPackage(bytes[1], bytes[2]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将数据包转化为字节数组
|
||||
/// </summary>
|
||||
/// <returns>字节数组</returns>
|
||||
public byte[] ToBytes()
|
||||
{
|
||||
var arr = new byte[4];
|
||||
|
||||
arr[0] = this.sign;
|
||||
arr[1] = this.commandID;
|
||||
arr[2] = this.resp;
|
||||
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue