From 4a55143b8e6a6b654320652bd4f4d12ec9c1f1cc Mon Sep 17 00:00:00 2001 From: SikongJueluo Date: Sun, 17 Aug 2025 17:01:42 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E6=88=90=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=97=8B=E8=BD=AC=E7=BC=96=E7=A0=81=E5=99=A8=E7=9A=84?= =?UTF-8?q?=E6=95=B0=E5=AD=97=E5=AD=AA=E7=94=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/Program.cs | 4 +- server/src/Hubs/RotaryEncoderHub.cs | 243 ++++++++ server/src/Peripherals/RotaryEncoderClient.cs | 69 +++ server/src/Peripherals/SwitchClient.cs | 4 - .../equipments/EC11RotaryEncoder.vue | 559 ++++++++++-------- src/stores/Peripherals/RotaryEncoder.ts | 92 +++ src/utils/AuthManager.ts | 2 +- .../Peripherals.RotaryEncoderClient.ts | 10 + .../signalR/TypedSignalR.Client/index.ts | 65 +- .../TypedSignalR.Client/server.Hubs.ts | 35 ++ 10 files changed, 818 insertions(+), 265 deletions(-) create mode 100644 server/src/Hubs/RotaryEncoderHub.cs create mode 100644 server/src/Peripherals/RotaryEncoderClient.cs create mode 100644 src/stores/Peripherals/RotaryEncoder.ts create mode 100644 src/utils/signalR/Peripherals.RotaryEncoderClient.ts diff --git a/server/Program.cs b/server/Program.cs index 682401e..9bfc86b 100644 --- a/server/Program.cs +++ b/server/Program.cs @@ -87,7 +87,8 @@ try if (!string.IsNullOrEmpty(accessToken) && ( path.StartsWithSegments("/hubs/JtagHub") || path.StartsWithSegments("/hubs/ProgressHub") || - path.StartsWithSegments("/hubs/DigitalTubesHub") + path.StartsWithSegments("/hubs/DigitalTubesHub") || + path.StartsWithSegments("/hubs/RotaryEncoderHub") )) { // Read the token out of the query string @@ -254,6 +255,7 @@ try app.MapHub("/hubs/JtagHub"); app.MapHub("/hubs/ProgressHub"); app.MapHub("/hubs/DigitalTubesHub"); + app.MapHub("/hubs/RotaryEncoderHub"); // Setup Program MsgBus.Init(); diff --git a/server/src/Hubs/RotaryEncoderHub.cs b/server/src/Hubs/RotaryEncoderHub.cs new file mode 100644 index 0000000..54d5a5c --- /dev/null +++ b/server/src/Hubs/RotaryEncoderHub.cs @@ -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 SetEnable(bool enable); + Task RotateEncoderOnce(int num, RotaryEncoderDirection direction); + Task EnableCycleRotateEncoder(int num, RotaryEncoderDirection direction, int freq); + Task 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, IRotaryEncoderHub +{ + private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + private readonly IHubContext _hubContext; + private readonly Database.UserManager _userManager = new(); + + private ConcurrentDictionary<(string, string), CycleTaskInfo> _cycleTasks = new(); + + public RotaryEncoderHub(IHubContext hubContext) + { + _hubContext = hubContext; + } + + private Optional 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 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 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 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 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}"); + } + }); + } +} diff --git a/server/src/Peripherals/RotaryEncoderClient.cs b/server/src/Peripherals/RotaryEncoderClient.cs new file mode 100644 index 0000000..078e744 --- /dev/null +++ b/server/src/Peripherals/RotaryEncoderClient.cs @@ -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> 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> 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; + } +} diff --git a/server/src/Peripherals/SwitchClient.cs b/server/src/Peripherals/SwitchClient.cs index 45a974f..273f01d 100644 --- a/server/src/Peripherals/SwitchClient.cs +++ b/server/src/Peripherals/SwitchClient.cs @@ -1,4 +1,3 @@ -using System.Collections; using System.Net; using DotNext; @@ -11,9 +10,6 @@ class SwitchCtrlAddr public const UInt32 ENABLE = BASE; } -/// -/// 矩阵键盘外设类,用于控制和管理矩阵键盘的功能。 -/// public class SwitchCtrl { private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); diff --git a/src/components/equipments/EC11RotaryEncoder.vue b/src/components/equipments/EC11RotaryEncoder.vue index f5d7d9a..19f0a5e 100644 --- a/src/components/equipments/EC11RotaryEncoder.vue +++ b/src/components/equipments/EC11RotaryEncoder.vue @@ -1,258 +1,301 @@ - - - - - - - + + + + + + + diff --git a/src/stores/Peripherals/RotaryEncoder.ts b/src/stores/Peripherals/RotaryEncoder.ts new file mode 100644 index 0000000..28a43b4 --- /dev/null +++ b/src/stores/Peripherals/RotaryEncoder.ts @@ -0,0 +1,92 @@ +import { AuthManager } from "@/utils/AuthManager"; +import type { RotaryEncoderDirection } from "@/utils/signalR/Peripherals.RotaryEncoderClient"; +import { + getHubProxyFactory, + getReceiverRegister, +} from "@/utils/signalR/TypedSignalR.Client"; +import type { + IRotaryEncoderHub, + IRotaryEncoderReceiver, +} from "@/utils/signalR/TypedSignalR.Client/server.Hubs"; +import { HubConnectionState, type HubConnection } from "@microsoft/signalr"; +import { isUndefined } from "mathjs"; +import { defineStore } from "pinia"; +import { onMounted, onUnmounted, ref, shallowRef } from "vue"; + +export const useRotaryEncoder = defineStore("RotaryEncoder", () => { + const rotaryEncoderHub = shallowRef<{ + connection: HubConnection; + proxy: IRotaryEncoderHub; + } | null>(null); + const rotaryEncoderReceiver: IRotaryEncoderReceiver = { + onReceiveRotate: async (data) => {}, + }; + + onMounted(() => { + initHub(); + }); + + onUnmounted(() => { + clearHub(); + }); + + function initHub() { + if (rotaryEncoderHub.value) return; + const connection = AuthManager.createHubConnection("RotaryEncoderHub"); + const proxy = + getHubProxyFactory("IRotaryEncoderHub").createHubProxy(connection); + getReceiverRegister("IRotaryEncoderReceiver").register( + connection, + rotaryEncoderReceiver, + ); + connection.start(); + rotaryEncoderHub.value = { connection, proxy }; + } + + function clearHub() { + if (!rotaryEncoderHub.value) return; + rotaryEncoderHub.value.connection.stop(); + rotaryEncoderHub.value = null; + } + + function reinitializeHub() { + clearHub(); + initHub(); + } + + function getHubProxy() { + if (!rotaryEncoderHub.value) throw new Error("Hub not initialized"); + return rotaryEncoderHub.value.proxy; + } + + async function setEnable(enabled: boolean) { + const proxy = getHubProxy(); + return await proxy.setEnable(enabled); + } + + async function rotateOnce(num: number, direction: RotaryEncoderDirection) { + const proxy = getHubProxy(); + return await proxy.rotateEncoderOnce(num, direction); + } + + async function enableCycleRotate( + num: number, + direction: RotaryEncoderDirection, + freq: number, + ) { + const proxy = getHubProxy(); + return await proxy.enableCycleRotateEncoder(num, direction, freq); + } + + async function disableCycleRotate() { + const proxy = getHubProxy(); + return await proxy.disableCycleRotateEncoder(); + } + + return { + setEnable, + rotateOnce, + enableCycleRotate, + disableCycleRotate, + }; +}); diff --git a/src/utils/AuthManager.ts b/src/utils/AuthManager.ts index 3aec628..bb9846d 100644 --- a/src/utils/AuthManager.ts +++ b/src/utils/AuthManager.ts @@ -45,7 +45,7 @@ export class AuthManager { // SignalR连接 - 简单明了 static createHubConnection( - hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub", + hubPath: "ProgressHub" | "JtagHub" | "DigitalTubesHub" | "RotaryEncoderHub", ) { return new HubConnectionBuilder() .withUrl(`http://127.0.0.1:5000/hubs/${hubPath}`, { diff --git a/src/utils/signalR/Peripherals.RotaryEncoderClient.ts b/src/utils/signalR/Peripherals.RotaryEncoderClient.ts new file mode 100644 index 0000000..bf97483 --- /dev/null +++ b/src/utils/signalR/Peripherals.RotaryEncoderClient.ts @@ -0,0 +1,10 @@ +/* THIS (.ts) FILE IS GENERATED BY Tapper */ +/* eslint-disable */ +/* tslint:disable */ + +/** Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderDirection */ +export enum RotaryEncoderDirection { + CounterClockwise = 0, + Clockwise = 1, +} + diff --git a/src/utils/signalR/TypedSignalR.Client/index.ts b/src/utils/signalR/TypedSignalR.Client/index.ts index 55e0eac..f740eae 100644 --- a/src/utils/signalR/TypedSignalR.Client/index.ts +++ b/src/utils/signalR/TypedSignalR.Client/index.ts @@ -3,8 +3,9 @@ /* tslint:disable */ // @ts-nocheck import type { HubConnection, IStreamResult, Subject } from '@microsoft/signalr'; -import type { IDigitalTubesHub, IJtagHub, IProgressHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver } from './server.Hubs'; +import type { IDigitalTubesHub, IJtagHub, IProgressHub, IRotaryEncoderHub, IDigitalTubesReceiver, IJtagReceiver, IProgressReceiver, IRotaryEncoderReceiver } from './server.Hubs'; import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs'; +import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient'; // components @@ -46,6 +47,7 @@ export type HubProxyFactoryProvider = { (hubType: "IDigitalTubesHub"): HubProxyFactory; (hubType: "IJtagHub"): HubProxyFactory; (hubType: "IProgressHub"): HubProxyFactory; + (hubType: "IRotaryEncoderHub"): HubProxyFactory; } export const getHubProxyFactory = ((hubType: string) => { @@ -58,12 +60,16 @@ export const getHubProxyFactory = ((hubType: string) => { if(hubType === "IProgressHub") { return IProgressHub_HubProxyFactory.Instance; } + if(hubType === "IRotaryEncoderHub") { + return IRotaryEncoderHub_HubProxyFactory.Instance; + } }) as HubProxyFactoryProvider; export type ReceiverRegisterProvider = { (receiverType: "IDigitalTubesReceiver"): ReceiverRegister; (receiverType: "IJtagReceiver"): ReceiverRegister; (receiverType: "IProgressReceiver"): ReceiverRegister; + (receiverType: "IRotaryEncoderReceiver"): ReceiverRegister; } export const getReceiverRegister = ((receiverType: string) => { @@ -76,6 +82,9 @@ export const getReceiverRegister = ((receiverType: string) => { if(receiverType === "IProgressReceiver") { return IProgressReceiver_Binder.Instance; } + if(receiverType === "IRotaryEncoderReceiver") { + return IRotaryEncoderReceiver_Binder.Instance; + } }) as ReceiverRegisterProvider; // HubProxy @@ -171,6 +180,39 @@ class IProgressHub_HubProxy implements IProgressHub { } } +class IRotaryEncoderHub_HubProxyFactory implements HubProxyFactory { + public static Instance = new IRotaryEncoderHub_HubProxyFactory(); + + private constructor() { + } + + public readonly createHubProxy = (connection: HubConnection): IRotaryEncoderHub => { + return new IRotaryEncoderHub_HubProxy(connection); + } +} + +class IRotaryEncoderHub_HubProxy implements IRotaryEncoderHub { + + public constructor(private connection: HubConnection) { + } + + public readonly setEnable = async (enable: boolean): Promise => { + return await this.connection.invoke("SetEnable", enable); + } + + public readonly rotateEncoderOnce = async (num: number, direction: RotaryEncoderDirection): Promise => { + return await this.connection.invoke("RotateEncoderOnce", num, direction); + } + + public readonly enableCycleRotateEncoder = async (num: number, direction: RotaryEncoderDirection, freq: number): Promise => { + return await this.connection.invoke("EnableCycleRotateEncoder", num, direction, freq); + } + + public readonly disableCycleRotateEncoder = async (): Promise => { + return await this.connection.invoke("DisableCycleRotateEncoder"); + } +} + // Receiver @@ -237,3 +279,24 @@ class IProgressReceiver_Binder implements ReceiverRegister { } } +class IRotaryEncoderReceiver_Binder implements ReceiverRegister { + + public static Instance = new IRotaryEncoderReceiver_Binder(); + + private constructor() { + } + + public readonly register = (connection: HubConnection, receiver: IRotaryEncoderReceiver): Disposable => { + + const __onReceiveRotate = (...args: [number, RotaryEncoderDirection]) => receiver.onReceiveRotate(...args); + + connection.on("OnReceiveRotate", __onReceiveRotate); + + const methodList: ReceiverMethod[] = [ + { methodName: "OnReceiveRotate", method: __onReceiveRotate } + ] + + return new ReceiverMethodSubscription(connection, methodList); + } +} + diff --git a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts index 3b17ecd..4385eb3 100644 --- a/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts +++ b/src/utils/signalR/TypedSignalR.Client/server.Hubs.ts @@ -4,6 +4,7 @@ // @ts-nocheck import type { IStreamResult, Subject } from '@microsoft/signalr'; import type { DigitalTubeTaskStatus, ProgressInfo } from '../server.Hubs'; +import type { RotaryEncoderDirection } from '../Peripherals.RotaryEncoderClient'; export type IDigitalTubesHub = { /** @@ -60,6 +61,31 @@ export type IProgressHub = { getProgress(taskId: string): Promise; } +export type IRotaryEncoderHub = { + /** + * @param enable Transpiled from bool + * @returns Transpiled from System.Threading.Tasks.Task + */ + setEnable(enable: boolean): Promise; + /** + * @param num Transpiled from int + * @param direction Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderDirection + * @returns Transpiled from System.Threading.Tasks.Task + */ + rotateEncoderOnce(num: number, direction: RotaryEncoderDirection): Promise; + /** + * @param num Transpiled from int + * @param direction Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderDirection + * @param freq Transpiled from int + * @returns Transpiled from System.Threading.Tasks.Task + */ + enableCycleRotateEncoder(num: number, direction: RotaryEncoderDirection, freq: number): Promise; + /** + * @returns Transpiled from System.Threading.Tasks.Task + */ + disableCycleRotateEncoder(): Promise; +} + export type IDigitalTubesReceiver = { /** * @param data Transpiled from byte[] @@ -84,3 +110,12 @@ export type IProgressReceiver = { onReceiveProgress(message: ProgressInfo): Promise; } +export type IRotaryEncoderReceiver = { + /** + * @param num Transpiled from int + * @param direction Transpiled from Peripherals.RotaryEncoderClient.RotaryEncoderDirection + * @returns Transpiled from System.Threading.Tasks.Task + */ + onReceiveRotate(num: number, direction: RotaryEncoderDirection): Promise; +} +