feat: 完成前后端旋转编码器的数字孪生
This commit is contained in:
243
server/src/Hubs/RotaryEncoderHub.cs
Normal file
243
server/src/Hubs/RotaryEncoderHub.cs
Normal file
@@ -0,0 +1,243 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.AspNetCore.Cors;
|
||||
using TypedSignalR.Client;
|
||||
using DotNext;
|
||||
using Peripherals.RotaryEncoderClient;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
#pragma warning disable 1998
|
||||
|
||||
namespace server.Hubs;
|
||||
|
||||
[Hub]
|
||||
public interface IRotaryEncoderHub
|
||||
{
|
||||
Task<bool> SetEnable(bool enable);
|
||||
Task<bool> RotateEncoderOnce(int num, RotaryEncoderDirection direction);
|
||||
Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq);
|
||||
Task<bool> DisableCycleRotateEncoder();
|
||||
}
|
||||
|
||||
[Receiver]
|
||||
public interface IRotaryEncoderReceiver
|
||||
{
|
||||
Task OnReceiveRotate(int num, RotaryEncoderDirection direction);
|
||||
}
|
||||
|
||||
public class CycleTaskInfo
|
||||
{
|
||||
public Task? CycleTask { get; set; }
|
||||
public RotaryEncoderCtrl EncoderClient { get; set; }
|
||||
public CancellationTokenSource CTS { get; set; } = new();
|
||||
public int Freq { get; set; }
|
||||
public int Num { get; set; }
|
||||
public RotaryEncoderDirection Direction { get; set; }
|
||||
|
||||
public CycleTaskInfo(
|
||||
RotaryEncoderCtrl client,
|
||||
int num, int freq,
|
||||
RotaryEncoderDirection direction)
|
||||
{
|
||||
EncoderClient = client;
|
||||
Num = num;
|
||||
Direction = direction;
|
||||
Freq = freq;
|
||||
}
|
||||
}
|
||||
|
||||
[Authorize]
|
||||
[EnableCors("SignalR")]
|
||||
public class RotaryEncoderHub : Hub<IRotaryEncoderReceiver>, IRotaryEncoderHub
|
||||
{
|
||||
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
private readonly IHubContext<RotaryEncoderHub, IRotaryEncoderReceiver> _hubContext;
|
||||
private readonly Database.UserManager _userManager = new();
|
||||
|
||||
private ConcurrentDictionary<(string, string), CycleTaskInfo> _cycleTasks = new();
|
||||
|
||||
public RotaryEncoderHub(IHubContext<RotaryEncoderHub, IRotaryEncoderReceiver> hubContext)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
}
|
||||
|
||||
private Optional<Database.Board> TryGetBoard()
|
||||
{
|
||||
var userName = Context.User?.FindFirstValue(ClaimTypes.Name);
|
||||
if (string.IsNullOrEmpty(userName))
|
||||
{
|
||||
logger.Error("User name is null or empty");
|
||||
return null;
|
||||
}
|
||||
|
||||
var userRet = _userManager.GetUserByName(userName);
|
||||
if (!userRet.IsSuccessful || !userRet.Value.HasValue)
|
||||
{
|
||||
logger.Error($"User '{userName}' not found");
|
||||
return null;
|
||||
}
|
||||
var user = userRet.Value.Value;
|
||||
|
||||
var boardRet = _userManager.GetBoardByID(user.BoardID);
|
||||
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
|
||||
{
|
||||
logger.Error($"Board not found");
|
||||
return null;
|
||||
}
|
||||
return boardRet.Value.Value;
|
||||
}
|
||||
|
||||
public async Task<bool> SetEnable(bool enable)
|
||||
{
|
||||
try
|
||||
{
|
||||
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
|
||||
var result = await encoderCtrl.SetEnable(enable);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error(result.Error, "SetEnable failed");
|
||||
return false;
|
||||
}
|
||||
return result.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to set enable");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RotateEncoderOnce(int num, RotaryEncoderDirection direction)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (num <= 0 || num > 4)
|
||||
throw new ArgumentException($"RotaryEncoder num should be 1~3, instead of {num}");
|
||||
|
||||
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
|
||||
var result = await encoderCtrl.RotateEncoderOnce(num, direction);
|
||||
if (!result.IsSuccessful)
|
||||
{
|
||||
logger.Error(result.Error, $"RotateEncoderOnce({num}, {direction}) failed");
|
||||
return false;
|
||||
}
|
||||
return result.Value;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to rotate encoder once");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (num <= 0 || num > 4) throw new ArgumentException(
|
||||
$"RotaryEncoder num should be 1~3, instead of {num}");
|
||||
|
||||
if (freq <= 0 || freq > 1000) throw new ArgumentException(
|
||||
$"Frequency should be between 1 and 1000, instead of {freq}");
|
||||
|
||||
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||
var key = (board.ID.ToString(), Context.ConnectionId);
|
||||
|
||||
if (_cycleTasks.TryGetValue(key, out var existing))
|
||||
await DisableCycleRotateEncoder();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var encoderCtrl = new RotaryEncoderCtrl(board.IpAddr, board.Port, 0);
|
||||
var cycleTaskInfo = new CycleTaskInfo(encoderCtrl, num, freq, direction);
|
||||
cycleTaskInfo.CycleTask = CycleRotate(cycleTaskInfo, Context.ConnectionId, board.ID.ToString());
|
||||
|
||||
_cycleTasks[key] = cycleTaskInfo;
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to enable cycle rotate encoder");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> DisableCycleRotateEncoder()
|
||||
{
|
||||
try
|
||||
{
|
||||
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
|
||||
var key = (board.ID.ToString(), Context.ConnectionId);
|
||||
|
||||
if (_cycleTasks.TryRemove(key, out var taskInfo))
|
||||
{
|
||||
taskInfo.CTS.Cancel();
|
||||
if (taskInfo.CycleTask != null)
|
||||
await taskInfo.CycleTask;
|
||||
taskInfo.CTS.Dispose();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error(ex, "Failed to disable cycle rotate encoder");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Task CycleRotate(CycleTaskInfo taskInfo, string clientId, string boardId)
|
||||
{
|
||||
var ctrl = taskInfo.EncoderClient;
|
||||
var token = taskInfo.CTS.Token;
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var cntError = 0;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
var ret = await ctrl.RotateEncoderOnce(taskInfo.Num, taskInfo.Direction);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error(
|
||||
$"Failed to rotate encoder {taskInfo.Num} on board {boardId}: {ret.Error}");
|
||||
cntError++;
|
||||
if (cntError >= 3)
|
||||
{
|
||||
logger.Error(
|
||||
$"Too many errors occurred while rotating encoder {taskInfo.Num} on board {boardId}");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ret.Value)
|
||||
{
|
||||
logger.Warn(
|
||||
$"Encoder {taskInfo.Num} on board {boardId} is not responding");
|
||||
continue;
|
||||
}
|
||||
|
||||
await _hubContext.Clients
|
||||
.Client(clientId)
|
||||
.OnReceiveRotate(taskInfo.Num, taskInfo.Direction);
|
||||
|
||||
await Task.Delay(1000 / taskInfo.Freq, token);
|
||||
}
|
||||
}, token)
|
||||
.ContinueWith((task) =>
|
||||
{
|
||||
if (task.IsFaulted)
|
||||
{
|
||||
logger.Error($"Rotary encoder cycle operation failed: {task.Exception}");
|
||||
}
|
||||
else if (task.IsCanceled)
|
||||
{
|
||||
logger.Info($"Rotary encoder cycle operation cancelled for board {boardId}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Info($"Rotary encoder cycle completed for board {boardId}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
69
server/src/Peripherals/RotaryEncoderClient.cs
Normal file
69
server/src/Peripherals/RotaryEncoderClient.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
using Tapper;
|
||||
|
||||
namespace Peripherals.RotaryEncoderClient;
|
||||
|
||||
class RotaryEncoderCtrlAddr
|
||||
{
|
||||
public const UInt32 BASE = 0xB0_00_00_20;
|
||||
|
||||
public const UInt32 ENABLE = BASE;
|
||||
}
|
||||
|
||||
[TranspilationSource]
|
||||
public enum RotaryEncoderDirection : uint
|
||||
{
|
||||
CounterClockwise = 0,
|
||||
Clockwise = 1,
|
||||
}
|
||||
|
||||
public class RotaryEncoderCtrl
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
readonly int timeout = 500;
|
||||
readonly int taskID;
|
||||
readonly int port;
|
||||
readonly string address;
|
||||
private IPEndPoint ep;
|
||||
|
||||
public RotaryEncoderCtrl(string address, int port, int taskID, int timeout = 500)
|
||||
{
|
||||
if (timeout < 0)
|
||||
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.ep = new IPEndPoint(IPAddress.Parse(address), port);
|
||||
this.taskID = taskID;
|
||||
this.timeout = timeout;
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> SetEnable(bool enable)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, RotaryEncoderCtrlAddr.ENABLE, enable ? 0x1U : 0x0U, this.timeout);
|
||||
if (!ret.IsSuccessful) return new(ret.Error);
|
||||
return ret.Value;
|
||||
}
|
||||
|
||||
public async ValueTask<Result<bool>> RotateEncoderOnce(int num, RotaryEncoderDirection direction)
|
||||
{
|
||||
if (MsgBus.IsRunning)
|
||||
MsgBus.UDPServer.ClearUDPData(this.address, this.taskID);
|
||||
else return new(new Exception("Message Bus not work!"));
|
||||
|
||||
var ret = await UDPClientPool.WriteAddr(
|
||||
this.ep, this.taskID, RotaryEncoderCtrlAddr.BASE + (UInt32)num, (UInt32)direction, this.timeout);
|
||||
if (!ret.IsSuccessful)
|
||||
{
|
||||
logger.Error($"Set Rotary Encoder {num} {direction.ToString()} failed: {ret.Error}");
|
||||
return new(ret.Error);
|
||||
}
|
||||
return ret.Value;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System.Collections;
|
||||
using System.Net;
|
||||
using DotNext;
|
||||
|
||||
@@ -11,9 +10,6 @@ class SwitchCtrlAddr
|
||||
public const UInt32 ENABLE = BASE;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。
|
||||
/// </summary>
|
||||
public class SwitchCtrl
|
||||
{
|
||||
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
|
||||
|
||||
Reference in New Issue
Block a user