feat: 使用SignalR实时发送示波器数据,并美化示波器界面

This commit is contained in:
2025-08-19 12:55:18 +08:00
parent 1b5b0e28e3
commit 7e53b805ae
13 changed files with 1664 additions and 347 deletions

View File

@@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;
using Peripherals.OscilloscopeClient;
using server.Hubs;
namespace server.Controllers;
@@ -9,6 +10,7 @@ namespace server.Controllers;
/// 示波器API控制器 - 普通用户权限
/// </summary>
[ApiController]
[EnableCors("Development")]
[Route("api/[controller]")]
[Authorize]
public class OscilloscopeApiController : ControllerBase
@@ -20,7 +22,7 @@ public class OscilloscopeApiController : ControllerBase
/// <summary>
/// 获取示波器实例
/// </summary>
private Oscilloscope? GetOscilloscope()
private OscilloscopeCtrl? GetOscilloscope()
{
try
{
@@ -41,7 +43,7 @@ public class OscilloscopeApiController : ControllerBase
return null;
var board = boardRet.Value.Value;
return new Oscilloscope(board.IpAddr, board.Port);
return new OscilloscopeCtrl(board.IpAddr, board.Port);
}
catch (Exception ex)
{
@@ -56,12 +58,11 @@ public class OscilloscopeApiController : ControllerBase
/// <param name="config">示波器配置</param>
/// <returns>操作结果</returns>
[HttpPost("Initialize")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<IActionResult> Initialize([FromBody] OscilloscopeFullConfig config)
public async Task<IActionResult> Initialize([FromBody] OscilloscopeConfig config)
{
try
{
@@ -151,7 +152,6 @@ public class OscilloscopeApiController : ControllerBase
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("StartCapture")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -185,7 +185,6 @@ public class OscilloscopeApiController : ControllerBase
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("StopCapture")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -219,7 +218,6 @@ public class OscilloscopeApiController : ControllerBase
/// </summary>
/// <returns>示波器数据和状态信息</returns>
[HttpGet("GetData")]
[EnableCors("Users")]
[ProducesResponseType(typeof(OscilloscopeDataResponse), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -293,7 +291,6 @@ public class OscilloscopeApiController : ControllerBase
/// <param name="risingEdge">触发边沿true为上升沿false为下降沿</param>
/// <returns>操作结果</returns>
[HttpPost("UpdateTrigger")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -338,7 +335,6 @@ public class OscilloscopeApiController : ControllerBase
/// <param name="decimationRate">抽样率0-1023</param>
/// <returns>操作结果</returns>
[HttpPost("UpdateSampling")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -387,7 +383,6 @@ public class OscilloscopeApiController : ControllerBase
/// </summary>
/// <returns>操作结果</returns>
[HttpPost("RefreshRAM")]
[EnableCors("Users")]
[ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
@@ -415,72 +410,4 @@ public class OscilloscopeApiController : ControllerBase
return StatusCode(StatusCodes.Status500InternalServerError, "操作失败,请稍后重试");
}
}
/// <summary>
/// 示波器完整配置
/// </summary>
public class OscilloscopeFullConfig
{
/// <summary>
/// 是否启动捕获
/// </summary>
public bool CaptureEnabled { get; set; }
/// <summary>
/// 触发电平0-255
/// </summary>
public byte TriggerLevel { get; set; }
/// <summary>
/// 触发边沿true为上升沿false为下降沿
/// </summary>
public bool TriggerRisingEdge { get; set; }
/// <summary>
/// 水平偏移量0-1023
/// </summary>
public ushort HorizontalShift { get; set; }
/// <summary>
/// 抽样率0-1023
/// </summary>
public ushort DecimationRate { get; set; }
/// <summary>
/// 是否自动刷新RAM
/// </summary>
public bool AutoRefreshRAM { get; set; } = true;
}
/// <summary>
/// 示波器状态和数据
/// </summary>
public class OscilloscopeDataResponse
{
/// <summary>
/// AD采样频率
/// </summary>
public uint ADFrequency { get; set; }
/// <summary>
/// AD采样幅度
/// </summary>
public byte ADVpp { get; set; }
/// <summary>
/// AD采样最大值
/// </summary>
public byte ADMax { get; set; }
/// <summary>
/// AD采样最小值
/// </summary>
public byte ADMin { get; set; }
/// <summary>
/// 波形数据Base64编码
/// </summary>
public string WaveformData { get; set; } = string.Empty;
}
}

View File

@@ -32,15 +32,9 @@ public class DigitalTubeTaskStatus
{
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public DigitalTubeTaskStatus(ScanTaskInfo info)
{
Frequency = info.Frequency;
IsRunning = info.IsRunning;
}
}
public class ScanTaskInfo
class DigitalTubesScanTaskInfo
{
public string BoardID { get; set; }
public string ClientID { get; set; }
@@ -50,13 +44,22 @@ public class ScanTaskInfo
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public ScanTaskInfo(
public DigitalTubesScanTaskInfo(
string boardID, string clientID, SevenDigitalTubesCtrl client)
{
BoardID = boardID;
ClientID = clientID;
TubeClient = client;
}
public DigitalTubeTaskStatus ToDigitalTubeTaskStatus()
{
return new DigitalTubeTaskStatus
{
Frequency = Frequency,
IsRunning = IsRunning
};
}
}
[Authorize]
@@ -67,7 +70,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
private readonly IHubContext<DigitalTubesHub, IDigitalTubesReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private ConcurrentDictionary<(string, string), ScanTaskInfo> _scanTasks = new();
private ConcurrentDictionary<(string, string), DigitalTubesScanTaskInfo> _scanTasks = new();
public DigitalTubesHub(IHubContext<DigitalTubesHub, IDigitalTubesReceiver> hubContext)
{
@@ -100,7 +103,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
return boardRet.Value.Value;
}
private Task ScanAllTubes(ScanTaskInfo scanInfo)
private Task ScanAllTubes(DigitalTubesScanTaskInfo scanInfo)
{
var token = scanInfo.CTS.Token;
return Task.Run(async () =>
@@ -163,7 +166,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
return true;
var cts = new CancellationTokenSource();
var scanTaskInfo = new ScanTaskInfo(
var scanTaskInfo = new DigitalTubesScanTaskInfo(
board.ID.ToString(), Context.ConnectionId,
new SevenDigitalTubesCtrl(board.IpAddr, board.Port, 0)
);
@@ -240,7 +243,7 @@ public class DigitalTubesHub : Hub<IDigitalTubesReceiver>, IDigitalTubesHub
if (_scanTasks.TryGetValue(key, out var scanInfo))
{
return new DigitalTubeTaskStatus(scanInfo);
return scanInfo.ToDigitalTubeTaskStatus();
}
else
{

View File

@@ -0,0 +1,386 @@
using Microsoft.AspNetCore.Authorization;
using System.Security.Claims;
using Microsoft.AspNetCore.SignalR;
using Microsoft.AspNetCore.Cors;
using TypedSignalR.Client;
using DotNext;
using Tapper;
using System.Collections.Concurrent;
using Peripherals.OscilloscopeClient;
#pragma warning disable 1998
namespace server.Hubs;
[Hub]
public interface IOscilloscopeHub
{
Task<bool> Initialize(OscilloscopeFullConfig config);
Task<bool> StartCapture();
Task<bool> StopCapture();
Task<OscilloscopeDataResponse?> GetData();
Task<bool> SetTrigger(byte level);
Task<bool> SetRisingEdge(bool risingEdge);
Task<bool> SetSampling(ushort decimationRate);
Task<bool> SetFrequency(int frequency);
}
[Receiver]
public interface IOscilloscopeReceiver
{
Task OnDataReceived(OscilloscopeDataResponse data);
}
[TranspilationSource]
public class OscilloscopeDataResponse
{
public uint ADFrequency { get; set; }
public byte ADVpp { get; set; }
public byte ADMax { get; set; }
public byte ADMin { get; set; }
public string WaveformData { get; set; } = "";
}
[TranspilationSource]
public class OscilloscopeFullConfig
{
public bool CaptureEnabled { get; set; }
public byte TriggerLevel { get; set; }
public bool TriggerRisingEdge { get; set; }
public ushort HorizontalShift { get; set; }
public ushort DecimationRate { get; set; }
public int CaptureFrequency { get; set; }
// public bool AutoRefreshRAM { get; set; }
public OscilloscopeConfig ToOscilloscopeConfig()
{
return new OscilloscopeConfig
{
CaptureEnabled = CaptureEnabled,
TriggerLevel = TriggerLevel,
TriggerRisingEdge = TriggerRisingEdge,
HorizontalShift = HorizontalShift,
DecimationRate = DecimationRate,
};
}
}
class OscilloscopeScanTaskInfo
{
public Task? ScanTask { get; set; }
public OscilloscopeCtrl Client { get; set; }
public CancellationTokenSource CTS { get; set; } = new();
public int Frequency { get; set; } = 100;
public bool IsRunning { get; set; } = false;
public OscilloscopeScanTaskInfo(OscilloscopeCtrl client)
{
Client = client;
}
}
[Authorize]
[EnableCors("SignalR")]
public class OscilloscopeHub : Hub<IOscilloscopeReceiver>, IOscilloscopeHub
{
private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
private readonly IHubContext<OscilloscopeHub, IOscilloscopeReceiver> _hubContext;
private readonly Database.UserManager _userManager = new();
private ConcurrentDictionary<(string, string), OscilloscopeScanTaskInfo> _scanTasks = new();
public OscilloscopeHub(IHubContext<OscilloscopeHub, IOscilloscopeReceiver> 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 boardRet = _userManager.GetBoardByUserName(userName);
if (!boardRet.IsSuccessful || !boardRet.Value.HasValue)
{
logger.Error($"Board not found");
return null;
}
return boardRet.Value.Value;
}
private Optional<OscilloscopeCtrl> GetOscilloscope()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = new OscilloscopeCtrl(board.IpAddr, board.Port, 0);
return client;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to get oscilloscope");
return null;
}
}
public async Task<bool> Initialize(OscilloscopeFullConfig config)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var result = await client.Init(config.ToOscilloscopeConfig());
if (!result.IsSuccessful)
{
logger.Error(result.Error, "Initialize failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to initialize oscilloscope");
return false;
}
}
public async Task<bool> StartCapture()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = (board.ID.ToString(), Context.ConnectionId);
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
if (_scanTasks.TryGetValue(key, out var existing) && existing.IsRunning)
return true;
var result = await client.SetCaptureEnable(true);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "StartCapture failed");
return false;
}
var scanTaskInfo = new OscilloscopeScanTaskInfo(client);
var token = scanTaskInfo.CTS.Token;
scanTaskInfo.ScanTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
var data = await GetData();
if (data == null)
{
logger.Error("GetData failed");
continue;
}
await Clients.Client(Context.ConnectionId).OnDataReceived(data);
await Task.Delay(1000 / scanTaskInfo.Frequency, token);
}
}, token);
_scanTasks[key] = scanTaskInfo;
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to start capture");
return false;
}
}
public async Task<bool> StopCapture()
{
try
{
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var key = (board.ID.ToString(), Context.ConnectionId);
if (_scanTasks.TryRemove(key, out var scanInfo))
{
scanInfo.IsRunning = false;
scanInfo.CTS.Cancel();
if (scanInfo.ScanTask != null) await scanInfo.ScanTask;
scanInfo.CTS.Dispose();
var result = await client.SetCaptureEnable(false);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "StopCapture failed");
return false;
}
return result.Value;
}
throw new Exception("Task not found");
}
catch (Exception ex)
{
logger.Error(ex, "Failed to stop capture");
return false;
}
}
public async Task<OscilloscopeDataResponse?> GetData()
{
try
{
var oscilloscope = GetOscilloscope().OrThrow(() => new Exception("用户未绑定有效的实验板"));
var freqResult = await oscilloscope.GetADFrequency();
var vppResult = await oscilloscope.GetADVpp();
var maxResult = await oscilloscope.GetADMax();
var minResult = await oscilloscope.GetADMin();
var waveformResult = await oscilloscope.GetWaveformData();
if (!freqResult.IsSuccessful)
{
logger.Error($"获取AD采样频率失败: {freqResult.Error}");
throw new Exception($"获取AD采样频率失败: {freqResult.Error}");
}
if (!vppResult.IsSuccessful)
{
logger.Error($"获取AD采样幅度失败: {vppResult.Error}");
throw new Exception($"获取AD采样幅度失败: {vppResult.Error}");
}
if (!maxResult.IsSuccessful)
{
logger.Error($"获取AD采样最大值失败: {maxResult.Error}");
throw new Exception($"获取AD采样最大值失败: {maxResult.Error}");
}
if (!minResult.IsSuccessful)
{
logger.Error($"获取AD采样最小值失败: {minResult.Error}");
throw new Exception($"获取AD采样最小值失败: {minResult.Error}");
}
if (!waveformResult.IsSuccessful)
{
logger.Error($"获取波形数据失败: {waveformResult.Error}");
throw new Exception($"获取波形数据失败: {waveformResult.Error}");
}
var response = new OscilloscopeDataResponse
{
ADFrequency = freqResult.Value,
ADVpp = vppResult.Value,
ADMax = maxResult.Value,
ADMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
return new OscilloscopeDataResponse
{
ADFrequency = freqResult.Value,
ADVpp = vppResult.Value,
ADMax = maxResult.Value,
ADMin = minResult.Value,
WaveformData = Convert.ToBase64String(waveformResult.Value)
};
}
catch (Exception ex)
{
logger.Error(ex, "获取示波器数据时发生异常");
return null;
}
}
public async Task<bool> SetTrigger(byte level)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var ret = await client.SetTriggerLevel(level);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error, "UpdateTrigger failed");
return false;
}
return ret.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to update trigger");
return false;
}
}
public async Task<bool> SetRisingEdge(bool risingEdge)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var ret = await client.SetTriggerEdge(risingEdge);
if (!ret.IsSuccessful)
{
logger.Error(ret.Error, "Update Rising Edge failed");
return false;
}
return ret.Value;
}
catch (Exception ex)
{
logger.Error(ex, "SetRisingEdge failed");
return false;
}
}
public async Task<bool> SetSampling(ushort decimationRate)
{
try
{
var client = GetOscilloscope().OrThrow(() => new Exception("Oscilloscope not found"));
var result = await client.SetDecimationRate(decimationRate);
if (!result.IsSuccessful)
{
logger.Error(result.Error, "UpdateSampling failed");
return false;
}
return result.Value;
}
catch (Exception ex)
{
logger.Error(ex, "Failed to update sampling");
return false;
}
}
public async Task<bool> SetFrequency(int frequency)
{
try
{
if (frequency < 1 || frequency > 1000)
return false;
var board = TryGetBoard().OrThrow(() => new Exception("Board not found"));
var key = (board.ID.ToString(), Context.ConnectionId);
if (_scanTasks.TryGetValue(key, out var scanInfo) && scanInfo.IsRunning)
{
scanInfo.Frequency = frequency;
return true;
}
else
{
logger.Warn($"SetFrequency called but no running scan for board {board.ID} and client {Context.ConnectionId}");
return false;
}
}
catch (Exception ex)
{
logger.Error(ex, "Failed to set frequency");
return false;
}
}
}

View File

@@ -2,9 +2,20 @@ using System.Net;
using Common;
using DotNext;
using WebProtocol;
using Tapper;
namespace Peripherals.OscilloscopeClient;
public class OscilloscopeConfig
{
public bool CaptureEnabled { get; set; }
public byte TriggerLevel { get; set; }
public bool TriggerRisingEdge { get; set; }
public ushort HorizontalShift { get; set; }
public ushort DecimationRate { get; set; }
// public bool AutoRefreshRAM { get; set; }
}
static class OscilloscopeAddr
{
const UInt32 BASE = 0x8000_0000;
@@ -71,7 +82,7 @@ static class OscilloscopeAddr
public const UInt32 RD_DATA_LENGTH = 0x0000_0400;
}
class Oscilloscope
class OscilloscopeCtrl
{
private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
@@ -88,7 +99,7 @@ class Oscilloscope
/// <param name="address">示波器设备IP地址</param>
/// <param name="port">示波器设备端口</param>
/// <param name="timeout">超时时间(毫秒)</param>
public Oscilloscope(string address, int port, int timeout = 2000)
public OscilloscopeCtrl(string address, int port, int timeout = 2000)
{
if (timeout < 0)
throw new ArgumentException("Timeout couldn't be negative", nameof(timeout));
@@ -98,6 +109,49 @@ class Oscilloscope
this.timeout = timeout;
}
/// <summary>
/// 一次性初始化/配置示波器
/// </summary>
/// <param name="config">完整配置</param>
/// <returns>操作结果全部成功返回true否则返回异常信息</returns>
public async ValueTask<Result<bool>> Init(OscilloscopeConfig config)
{
// 1. 捕获使能
var ret = await SetCaptureEnable(config.CaptureEnabled);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set capture enable"));
// 2. 触发电平
ret = await SetTriggerLevel(config.TriggerLevel);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set trigger level"));
// 3. 触发边沿
ret = await SetTriggerEdge(config.TriggerRisingEdge);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set trigger edge"));
// 4. 水平偏移
ret = await SetHorizontalShift(config.HorizontalShift);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set horizontal shift"));
// 5. 抽样率
ret = await SetDecimationRate(config.DecimationRate);
if (!ret.IsSuccessful || !ret.Value)
return new(ret.Error ?? new Exception("Failed to set decimation rate"));
// 6. RAM刷新如果需要
// if (config.AutoRefreshRAM)
// {
// ret = await RefreshRAM();
// if (!ret.IsSuccessful || !ret.Value)
// return new(ret.Error ?? new Exception("Failed to refresh RAM"));
// }
return true;
}
/// <summary>
/// 控制示波器的捕获开关
/// </summary>
@@ -309,13 +363,13 @@ class Oscilloscope
// 等待WAVE_READY[0]位为1最多等待50ms5次x10ms间隔
var readyResult = await UDPClientPool.ReadAddrWithWait(
this.ep, this.taskID, OscilloscopeAddr.WAVE_READY, 0b00, 0x01, 10, 50);
if (!readyResult.IsSuccessful)
{
logger.Error($"Failed to wait for wave ready: {readyResult.Error}");
return new(readyResult.Error);
}
// 无论准备好与否都继续读取数据readyResult.Value表示是否在超时前准备好
if (!readyResult.Value)
{
@@ -365,14 +419,14 @@ class Oscilloscope
logger.Error("ReadAddr returned invalid data for trigger position");
return new(new Exception("Failed to read trigger position"));
}
UInt32 trigAddr = Number.BytesToUInt32(trigPosResult.Value.Options.Data).Value;
// 根据触发地址对数据进行偏移,使触发点位于数据中间
int targetPos = sampleCount / 2; // 目标位置:数据中间
int actualTrigPos = (int)(trigAddr % (UInt32)sampleCount); // 实际触发位置
int shiftAmount = targetPos - actualTrigPos;
// 创建偏移后的数据数组
byte[] offsetData = new byte[sampleCount];
for (int i = 0; i < sampleCount; i++)