Initial commit
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
using System.Linq;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho Battery device - cung cấp real-time battery data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class BatteryHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IBattery)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy thông tin battery theo device ID
|
||||
/// </summary>
|
||||
public async Task<BatteryState?> GetBatteryData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IBattery battery)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var data = await battery.ReadBatteryStateAsync();
|
||||
return SanitizeBatteryState(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize BatteryState to ensure all double values are valid for JSON serialization
|
||||
/// Replaces NaN and Infinity with 0.0
|
||||
/// </summary>
|
||||
private static BatteryState SanitizeBatteryState(BatteryState state)
|
||||
{
|
||||
var sanitized = state;
|
||||
|
||||
// Sanitize single double values
|
||||
sanitized.Voltage = SanitizeFloat(state.Voltage);
|
||||
sanitized.Current = SanitizeFloat(state.Current);
|
||||
sanitized.Charge = SanitizeFloat(state.Charge);
|
||||
sanitized.Capacity = SanitizeFloat(state.Capacity);
|
||||
sanitized.DesignCapacity = SanitizeFloat(state.DesignCapacity);
|
||||
sanitized.Percentage = SanitizeFloat(state.Percentage);
|
||||
|
||||
// Sanitize double arrays
|
||||
if (state.CellVoltage != null && state.CellVoltage.Length > 0)
|
||||
{
|
||||
sanitized.CellVoltage = state.CellVoltage.Select(SanitizeFloat).ToArray();
|
||||
}
|
||||
|
||||
if (state.CellTemperature != null && state.CellTemperature.Length > 0)
|
||||
{
|
||||
sanitized.CellTemperature = state.CellTemperature.Select(SanitizeFloat).ToArray();
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitize a double value: replace NaN and Infinity with 0.0
|
||||
/// </summary>
|
||||
private static double SanitizeFloat(double value)
|
||||
{
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
using RobotNet10.Shared.Geometry;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho Camera QR device - cung cấp real-time QR detection data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class CameraQrHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICameraQr)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy dữ liệu Camera QR theo device ID
|
||||
/// </summary>
|
||||
public async Task<CameraQrDataDto?> GetCameraQrData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICameraQr cameraQr)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapToDto(cameraQr);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map ICameraQr sang CameraQrDataDto
|
||||
/// </summary>
|
||||
private static CameraQrDataDto MapToDto(ICameraQr cameraQr)
|
||||
{
|
||||
return new CameraQrDataDto
|
||||
{
|
||||
IsConnected = cameraQr.IsConnected,
|
||||
Codes = cameraQr.Codes,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,556 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.CANOpen.CiA402.Enums;
|
||||
using RobotNet10.CANOpen.CiA402.Models;
|
||||
using RobotNet10.CANOpen.Exceptions;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho CiA402Servo device - cung cấp real-time servo data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class CiA402ServoHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy dữ liệu servo theo device ID
|
||||
/// </summary>
|
||||
public async Task<CiA402ServoDataDto?> GetServoData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await MapToDtoAsync(servo);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map ICiA402Servo sang CiA402ServoDataDto. Các lệnh đọc SDO (GetOperationMode, GetLatestErrorCode, GetProfile*) có thể timeout
|
||||
/// khi bus bận hoặc drive không phản hồi — bắt exception và trả về dữ liệu từ cache + giá trị mặc định.
|
||||
/// </summary>
|
||||
private static async Task<CiA402ServoDataDto> MapToDtoAsync(ICiA402Servo servo)
|
||||
{
|
||||
var statusword = servo.CachedStatusword;
|
||||
var driveState = statusword.GetState();
|
||||
var position = servo.CachedPosition;
|
||||
var velocity = servo.CachedVelocity;
|
||||
var torque = servo.CachedTorque;
|
||||
|
||||
OperationMode operationMode = OperationMode.ProfilePosition;
|
||||
try
|
||||
{
|
||||
operationMode = await servo.GetOperationModeAsync();
|
||||
}
|
||||
catch (CanOpenTimeoutException)
|
||||
{
|
||||
// SDO timeout (vd. 0x6061) — dùng mặc định, tránh fail hub
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Lỗi khác khi đọc mode — giữ mặc định
|
||||
}
|
||||
|
||||
ushort errorCode = 0;
|
||||
try
|
||||
{
|
||||
errorCode = await servo.GetLatestErrorCodeAsync();
|
||||
}
|
||||
catch (CanOpenTimeoutException)
|
||||
{
|
||||
// SDO timeout — giữ 0
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Bỏ qua
|
||||
}
|
||||
|
||||
uint profileSpeed = 0, profileAcceleration = 0, profileDeceleration = 0;
|
||||
try
|
||||
{
|
||||
profileSpeed = await servo.GetProfileSpeedAsync();
|
||||
profileAcceleration = await servo.GetProfileAccelerationAsync();
|
||||
profileDeceleration = await servo.GetProfileDecelerationAsync();
|
||||
}
|
||||
catch (CanOpenTimeoutException)
|
||||
{
|
||||
// Timeout — giữ 0
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Nếu drive chưa sẵn sàng đọc, giữ 0
|
||||
}
|
||||
|
||||
return new CiA402ServoDataDto
|
||||
{
|
||||
Statusword = statusword.Value,
|
||||
DriveState = driveState.ToString(),
|
||||
OperationMode = operationMode.ToString(),
|
||||
Position = position,
|
||||
Velocity = velocity,
|
||||
Torque = torque,
|
||||
ErrorCode = errorCode,
|
||||
IsConnected = servo is not DeviceBase deviceBase || deviceBase.IsConnected,
|
||||
ProfileSpeed = profileSpeed,
|
||||
ProfileAcceleration = profileAcceleration,
|
||||
ProfileDeceleration = profileDeceleration
|
||||
};
|
||||
}
|
||||
|
||||
// State Machine Control Methods
|
||||
public async Task EnableOperationAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.EnableOperationAsync();
|
||||
}
|
||||
|
||||
public async Task DisableOperationAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.DisableOperationAsync();
|
||||
}
|
||||
|
||||
public async Task QuickStopAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.QuickStopAsync();
|
||||
}
|
||||
|
||||
public async Task FaultResetAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.FaultResetAsync();
|
||||
}
|
||||
|
||||
public async Task ShutdownAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.ShutdownAsync();
|
||||
}
|
||||
|
||||
public async Task SwitchOnAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SwitchOnAsync();
|
||||
}
|
||||
|
||||
public async Task EnableAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.EnableAsync();
|
||||
}
|
||||
|
||||
public async Task DisableAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.DisableAsync();
|
||||
}
|
||||
|
||||
// Operation Mode
|
||||
public async Task SetOperationModeAsync(string deviceId, string modeString)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
if (!Enum.TryParse<OperationMode>(modeString, ignoreCase: true, out var mode))
|
||||
throw new ArgumentException($"Invalid operation mode: {modeString}");
|
||||
|
||||
await servo.SetOperationModeAsync(mode);
|
||||
}
|
||||
|
||||
// Position Control
|
||||
public async Task SetTargetPositionAsync(string deviceId, int position)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetTargetPositionAsync(position);
|
||||
}
|
||||
|
||||
public async Task MoveToPositionAsync(string deviceId, int position, uint velocity = 1000, uint acceleration = 5000, uint deceleration = 5000)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.MoveToPositionAsync(position, velocity, acceleration, deceleration);
|
||||
}
|
||||
|
||||
// Velocity Control
|
||||
public async Task SetTargetVelocityAsync(string deviceId, int velocity)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetTargetVelocityAsync(velocity);
|
||||
}
|
||||
|
||||
public async Task TargetVelocityAsync(string deviceId, int targetVelocity, uint acceleration = 5000, uint deceleration = 5000)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.TargetVelocityAsync(targetVelocity, acceleration, deceleration);
|
||||
}
|
||||
|
||||
public async Task ProfileVelocityAsync(string deviceId, int targetVelocity, uint acceleration = 5000, uint deceleration = 5000)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.ProfileVelocityAsync(targetVelocity, acceleration, deceleration);
|
||||
}
|
||||
|
||||
// Torque Control
|
||||
public async Task SetTargetTorqueAsync(string deviceId, short torque)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetTargetTorqueAsync(torque);
|
||||
}
|
||||
|
||||
public async Task RunTorqueAsync(string deviceId, short torque)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.RunTorqueAsync(torque);
|
||||
}
|
||||
|
||||
// Profile Settings
|
||||
public async Task SetProfileAccelerationAsync(string deviceId, uint acceleration)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetProfileAccelerationAsync(acceleration);
|
||||
}
|
||||
|
||||
public async Task SetProfileDecelerationAsync(string deviceId, uint deceleration)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetProfileDecelerationAsync(deceleration);
|
||||
}
|
||||
|
||||
public async Task SetProfileVelocityAsync(string deviceId, uint velocity)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetProfileVelocityAsync(velocity);
|
||||
}
|
||||
|
||||
public async Task SetProfileSpeedAsync(string deviceId, uint velocity)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetProfileSpeedAsync(velocity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ghi cùng lúc cả 3 profile (speed, acceleration, deceleration) xuống drive qua SDO.
|
||||
/// </summary>
|
||||
public async Task SetProfileSettingsAsync(string deviceId, uint profileSpeed, uint profileAcceleration, uint profileDeceleration)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetProfileSpeedAsync(profileSpeed);
|
||||
await servo.SetProfileAccelerationAsync(profileAcceleration);
|
||||
await servo.SetProfileDecelerationAsync(profileDeceleration);
|
||||
}
|
||||
|
||||
// Homing
|
||||
public async Task SetHomingMethodAsync(string deviceId, byte method)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetHomingMethodAsync(method);
|
||||
}
|
||||
|
||||
public async Task SetHomingSpeedAsync(string deviceId, int speed)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetHomingSpeedAsync(speed);
|
||||
}
|
||||
|
||||
public async Task SetHomingOffsetAsync(string deviceId, int offset)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.SetHomingOffsetAsync(offset);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing method từ drive để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
public async Task<byte> GetHomingMethodAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
return await servo.GetHomingMethodAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing speed từ drive để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
public async Task<int> GetHomingSpeedAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
return await servo.GetHomingSpeedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Đọc lại homing offset từ drive để kiểm tra đã ghi xuống chưa.
|
||||
/// </summary>
|
||||
public async Task<int> GetHomingOffsetAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
return await servo.GetHomingOffsetAsync();
|
||||
}
|
||||
|
||||
public async Task StartHomingAsync(string deviceId, byte method, int speed)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.StartHomingAsync(method, speed);
|
||||
}
|
||||
|
||||
// Position Control - Additional Methods
|
||||
public async Task StartPositionMoveAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.StartPositionMoveAsync();
|
||||
}
|
||||
|
||||
public async Task WaitUntilAtTargetAsync(string deviceId, int tolerance = 100, bool useStatusword = true, int checkIntervalMs = 10)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
await servo.WaitUntilAtTargetAsync(tolerance, useStatusword, checkIntervalMs);
|
||||
}
|
||||
|
||||
// Error and Status Information
|
||||
public async Task<bool> IsInFaultStateAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.IsInFaultStateAsync();
|
||||
}
|
||||
|
||||
public async Task<byte> GetErrorRegisterAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetErrorRegisterAsync();
|
||||
}
|
||||
|
||||
public async Task<ushort[]> GetErrorHistoryAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetErrorHistoryAsync();
|
||||
}
|
||||
|
||||
public async Task<ushort> GetLatestErrorCodeAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetLatestErrorCodeAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> TryFaultResetAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.TryFaultResetAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsEnabledAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.IsEnabledAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> IsReadyAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.IsReadyAsync();
|
||||
}
|
||||
|
||||
// Statusword & Controlword
|
||||
public async Task<ushort> GetStatuswordAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
var statusword = await servo.GetStatuswordAsync();
|
||||
return statusword.Value;
|
||||
}
|
||||
|
||||
public async Task SetControlwordAsync(string deviceId, ushort controlwordValue)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
var controlword = new Controlword(controlwordValue);
|
||||
await servo.SetControlwordAsync(controlword);
|
||||
}
|
||||
|
||||
public async Task<string> GetStateAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
var state = await servo.GetStateAsync();
|
||||
return state.ToString();
|
||||
}
|
||||
|
||||
public async Task<string> GetOperationModeAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
var mode = await servo.GetOperationModeAsync();
|
||||
return mode.ToString();
|
||||
}
|
||||
|
||||
// Position, Velocity, Torque
|
||||
public async Task<int> GetActualPositionAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetActualPositionAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetActualVelocityAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetActualVelocityAsync();
|
||||
}
|
||||
|
||||
public async Task<short> GetActualTorqueAsync(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return await servo.GetActualTorqueAsync();
|
||||
}
|
||||
|
||||
// Target Position Checking
|
||||
public Task<bool> IsAtTarget(string deviceId, int tolerance = 100, bool useStatusword = true)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ICiA402Servo servo)
|
||||
throw new InvalidOperationException($"Device {deviceId} is not a CiA402Servo");
|
||||
|
||||
return Task.FromResult(servo.IsAtTarget(tolerance, useStatusword));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho DeviceProvider - cung cấp real-time device information
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class DeviceHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy tất cả devices
|
||||
/// </summary>
|
||||
public Task<DeviceDto[]> GetAllDevices()
|
||||
{
|
||||
var devices = deviceProvider.GetAllDevices();
|
||||
return Task.FromResult(devices.Select(MapToDto).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy device theo ID
|
||||
/// </summary>
|
||||
public Task<DeviceDto?> GetDevice(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
return Task.FromResult(device != null ? MapToDto(device) : null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy devices theo type
|
||||
/// </summary>
|
||||
public Task<DeviceDto[]> GetDevicesByType(DeviceType deviceType)
|
||||
{
|
||||
var serverType = deviceType;
|
||||
var devices = deviceProvider.GetDevicesByType(serverType);
|
||||
return Task.FromResult(devices.Select(MapToDto).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy số lượng devices
|
||||
/// </summary>
|
||||
public Task<int> GetDeviceCount()
|
||||
{
|
||||
return Task.FromResult(deviceProvider.GetDeviceCount());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map DeviceBase sang DeviceDto
|
||||
/// </summary>
|
||||
private static DeviceDto MapToDto(DeviceBase device)
|
||||
{
|
||||
return new DeviceDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName,
|
||||
DeviceType = device.Type,
|
||||
Description = device.Description,
|
||||
Status = device.Status,
|
||||
IsConnected = device.IsConnected,
|
||||
LastUpdateTime = device.LastUpdateStateTime,
|
||||
LastConnectedTime = device.LastConnectedTime,
|
||||
LastDisconnectedTime = device.LastDisconnectedTime,
|
||||
LastError = device.LastError?.Message,
|
||||
ReconnectAttemptCount = device.ReconnectAttemptCount,
|
||||
AutoReconnectEnabled = device.AutoReconnectEnabled,
|
||||
ReconnectDelayMs = device.ReconnectDelayMs,
|
||||
MaxReconnectAttempts = device.MaxReconnectAttempts,
|
||||
PropertyDescriptions = device.PropertyDescriptions.Select(MapPropertyDescription).ToList(),
|
||||
Properties = device.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map PropertyDescription sang PropertyDescriptionDto
|
||||
/// </summary>
|
||||
private static PropertyDescription MapPropertyDescription(PropertyDescription prop)
|
||||
{
|
||||
return new PropertyDescription
|
||||
{
|
||||
Key = prop.Key,
|
||||
DisplayName = prop.DisplayName,
|
||||
Description = prop.Description,
|
||||
DataType = prop.DataType,
|
||||
Unit = prop.Unit,
|
||||
DefaultValue = prop.DefaultValue,
|
||||
IsReadOnly = prop.IsReadOnly,
|
||||
DisplayOrder = prop.DisplayOrder,
|
||||
Category = prop.Category,
|
||||
Format = prop.Format
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// HubContext để broadcast device updates từ DeviceProvider
|
||||
/// </summary>
|
||||
public class DeviceHubContext(IHubContext<DeviceHub> hubContext)
|
||||
{
|
||||
/// <summary>
|
||||
/// Broadcast device update đến tất cả clients
|
||||
/// </summary>
|
||||
public async Task BroadcastDeviceUpdate(DeviceBase device)
|
||||
{
|
||||
var update = new DeviceUpdateDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
Status = device.Status,
|
||||
Properties = device.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
|
||||
LastError = device.LastError?.Message,
|
||||
LastUpdateTime = device.LastUpdateStateTime,
|
||||
LastConnectedTime = device.LastConnectedTime,
|
||||
LastDisconnectedTime = device.LastDisconnectedTime,
|
||||
ReconnectAttemptCount = device.ReconnectAttemptCount
|
||||
};
|
||||
|
||||
await hubContext.Clients.All.SendAsync("DeviceUpdated", update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast device status change
|
||||
/// </summary>
|
||||
public async Task BroadcastDeviceStatusChanged(string deviceId, DeviceStatus status)
|
||||
{
|
||||
await hubContext.Clients.All.SendAsync("DeviceStatusChanged", deviceId, status);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.Shared.Geometry;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho IMU device - cung cấp real-time IMU data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class InertialMeasurementUnitHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IInertialMeasurementUnit)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy thông tin IMU theo device ID
|
||||
/// </summary>
|
||||
public async Task<Imu?> GetImuData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IInertialMeasurementUnit imu)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await imu.ReadAllDataAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Đọc tất cả dữ liệu IMU
|
||||
/// </summary>
|
||||
public async Task<Imu?> ReadAllData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IInertialMeasurementUnit imu)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await imu.ReadAllDataAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy dictionary properties hiện tại của device (để hiển thị trên UI)
|
||||
/// </summary>
|
||||
public Task<Dictionary<string, string>?> GetDeviceProperties(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IInertialMeasurementUnit)
|
||||
{
|
||||
return Task.FromResult<Dictionary<string, string>?>(null);
|
||||
}
|
||||
|
||||
var props = device.Properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
return Task.FromResult<Dictionary<string, string>?>(props);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy danh sách mô tả properties của device (để hiển thị trên UI)
|
||||
/// </summary>
|
||||
public Task<List<PropertyDescription>?> GetDevicePropertyDescriptions(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IInertialMeasurementUnit)
|
||||
{
|
||||
return Task.FromResult<List<PropertyDescription>?>(null);
|
||||
}
|
||||
|
||||
var list = device.PropertyDescriptions
|
||||
.Select(p => new PropertyDescription
|
||||
{
|
||||
Key = p.Key,
|
||||
DisplayName = p.DisplayName,
|
||||
Description = p.Description,
|
||||
DataType = p.DataType,
|
||||
Unit = p.Unit,
|
||||
DefaultValue = p.DefaultValue,
|
||||
IsReadOnly = p.IsReadOnly,
|
||||
DisplayOrder = p.DisplayOrder,
|
||||
Category = p.Category,
|
||||
Format = p.Format
|
||||
})
|
||||
.ToList();
|
||||
return Task.FromResult<List<PropertyDescription>?>(list);
|
||||
}
|
||||
}
|
||||
|
||||
145
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/LidarHub.cs
Normal file
145
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/LidarHub.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho Lidar device - cung cấp real-time lidar scan data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class LidarHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Độ phân giải mới: 1 độ (1 tia/độ) tính bằng radian
|
||||
/// </summary>
|
||||
private const double TARGET_ANGLE_INCREMENT_RAD = (Math.PI / 180.0); // 1 độ = π/180 radian
|
||||
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ILidar)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy thông tin lidar scan data theo device ID
|
||||
/// Trả về LaserScan với độ phân giải mới 1 độ (1 tia/độ)
|
||||
/// </summary>
|
||||
public async Task<LaserScan?> GetLidarData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not ILidar lidar || lidar.CurrentMeasurementData == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var originalScan = lidar.CurrentMeasurementData.Value;
|
||||
|
||||
// Tạo LaserScan mới với độ phân giải 1°
|
||||
var newScan = new LaserScan
|
||||
{
|
||||
Header = originalScan.Header,
|
||||
AngleMin = originalScan.AngleMin,
|
||||
AngleMax = originalScan.AngleMax,
|
||||
AngleIncrement = TARGET_ANGLE_INCREMENT_RAD, // 1° spacing (π/180 rad)
|
||||
TimeIncrement = originalScan.TimeIncrement,
|
||||
ScanTime = originalScan.ScanTime,
|
||||
RangeMin = originalScan.RangeMin,
|
||||
RangeMax = originalScan.RangeMax
|
||||
};
|
||||
|
||||
// Tính số điểm mới với độ phân giải 1 độ
|
||||
var angleSpan = originalScan.AngleMax - originalScan.AngleMin;
|
||||
var newPointCount = (int)Math.Round(angleSpan / TARGET_ANGLE_INCREMENT_RAD) + 1;
|
||||
newScan.Ranges = new double[newPointCount];
|
||||
newScan.Intensities = new double[newPointCount];
|
||||
|
||||
// Tính toán lại Ranges và Intensities với độ phân giải mới
|
||||
for (int i = 0; i < newPointCount; i++)
|
||||
{
|
||||
// Tính góc bắt đầu và kết thúc cho bin hiện tại
|
||||
var targetAngleStart = originalScan.AngleMin + i * TARGET_ANGLE_INCREMENT_RAD;
|
||||
var targetAngleEnd = Math.Min(
|
||||
originalScan.AngleMin + (i + 1) * TARGET_ANGLE_INCREMENT_RAD,
|
||||
originalScan.AngleMax
|
||||
);
|
||||
|
||||
// Tìm các index trong khoảng [targetAngleStart, targetAngleEnd]
|
||||
var indexStart = (targetAngleStart - originalScan.AngleMin) / originalScan.AngleIncrement;
|
||||
var indexEnd = (targetAngleEnd - originalScan.AngleMin) / originalScan.AngleIncrement;
|
||||
|
||||
var index0 = (int)Math.Floor(indexStart);
|
||||
var index1 = (int)Math.Ceiling(indexEnd);
|
||||
|
||||
// Clamp indices to valid range
|
||||
index0 = Math.Max(0, Math.Min(index0, originalScan.Ranges.Length - 1));
|
||||
index1 = Math.Max(0, Math.Min(index1, originalScan.Ranges.Length - 1));
|
||||
|
||||
// Tính trung bình các điểm có intensity > 0 trong khoảng [index0, index1]
|
||||
double sumRange = 0;
|
||||
double sumIntensity = 0;
|
||||
int validPointCount = 0;
|
||||
|
||||
for (int idx = index0; idx <= index1; idx++)
|
||||
{
|
||||
// Kiểm tra intensity để xác định điểm hợp lệ
|
||||
bool hasValidIntensity = originalScan.Intensities != null &&
|
||||
idx < originalScan.Intensities.Length &&
|
||||
originalScan.Intensities[idx] > 0;
|
||||
|
||||
if (hasValidIntensity)
|
||||
{
|
||||
var range = originalScan.Ranges[idx];
|
||||
|
||||
// Chỉ lấy các điểm không phải NaN/Infinity
|
||||
if (!double.IsNaN(range) && !double.IsInfinity(range))
|
||||
{
|
||||
sumRange += range;
|
||||
sumIntensity += originalScan.Intensities![idx];
|
||||
validPointCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tính giá trị trung bình hoặc set -1 nếu không có điểm hợp lệ
|
||||
if (validPointCount > 0)
|
||||
{
|
||||
newScan.Ranges[i] = sumRange / validPointCount;
|
||||
newScan.Intensities[i] = sumIntensity / validPointCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
newScan.Ranges[i] = -1.0; // No valid data
|
||||
newScan.Intensities[i] = 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Final safety pass: ensure no NaN/Infinity values remain
|
||||
// This should ideally never catch anything if the logic above is correct
|
||||
for (int i = 0; i < newScan.Ranges.Length; i++)
|
||||
{
|
||||
if (double.IsNaN(newScan.Ranges[i]) || double.IsInfinity(newScan.Ranges[i]))
|
||||
{
|
||||
newScan.Ranges[i] = -1.0;
|
||||
}
|
||||
}
|
||||
|
||||
return newScan;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Detection;
|
||||
using RobotNet10.Shared;
|
||||
using RobotNet10.Shared.Detection;
|
||||
using RobotNet10.Shared.Geometry;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class MarkerDetectorHub(IMarkerDetector markerDetector, ILogger<MarkerDetectorHub> logger) : Hub
|
||||
{
|
||||
private const string SessionKey = "DetectSessionId";
|
||||
|
||||
public async Task<MessageResult<Guid>> CreateSession(MarkersSearchRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If this client already has a session, dispose it first
|
||||
if (Context.Items.TryGetValue(SessionKey, out var existing) && existing is Guid existingSessionId)
|
||||
{
|
||||
var existingSession = markerDetector.GetSession(existingSessionId);
|
||||
existingSession?.Dispose();
|
||||
Context.Items.Remove(SessionKey);
|
||||
logger.LogInformation("Disposed existing session {SessionId} for client {ConnectionId}", existingSessionId, Context.ConnectionId);
|
||||
}
|
||||
|
||||
request.Yaw = Math.PI * request.Yaw / 180.0;
|
||||
var session = await markerDetector.CreateSessionAsync(request);
|
||||
Context.Items[SessionKey] = session.SessionId;
|
||||
|
||||
logger.LogInformation("Created session {SessionId} for client {ConnectionId}", session.SessionId, Context.ConnectionId);
|
||||
return new MessageResult<Guid>(true, session.SessionId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogError(ex, "Failed to create detect session for client {ConnectionId}", Context.ConnectionId);
|
||||
return new MessageResult<Guid>(false, Message: ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public MessageResult<Pose> GetGoal(Guid sessionId)
|
||||
{
|
||||
var session = markerDetector.GetSession(sessionId);
|
||||
if (session == null)
|
||||
return new(false, Message: $"Session '{sessionId}' not found.");
|
||||
|
||||
if (session.Goal.HasValue)
|
||||
{
|
||||
return new(true, session.Goal.Value.Pose);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new(false);
|
||||
}
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (Context.Items.TryGetValue(SessionKey, out var existing) && existing is Guid sessionId)
|
||||
{
|
||||
var session = markerDetector.GetSession(sessionId);
|
||||
session?.Dispose();
|
||||
logger.LogInformation("Disposed session {SessionId} on disconnect of client {ConnectionId}", sessionId, Context.ConnectionId);
|
||||
}
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
using RobotNet10.Shared;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho ModbusTCP device - cung cấp real-time Modbus data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class ModbusTcpHub(IDeviceProvider deviceProvider) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy thông tin device (DeviceName) theo device ID
|
||||
/// </summary>
|
||||
public async Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IModbusTcpDevice)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new DeviceInfoDto
|
||||
{
|
||||
DeviceId = device.DeviceId,
|
||||
DeviceName = device.DeviceName
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tất cả dữ liệu Modbus theo device ID
|
||||
/// </summary>
|
||||
public async Task<ModbusTcpData?> GetModbusData(string deviceId)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IModbusTcpDevice modbusDevice)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapToDto(modbusDevice);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ghi một coil theo device ID
|
||||
/// </summary>
|
||||
/// <param name="deviceId">Device ID</param>
|
||||
/// <param name="address">Địa chỉ coil</param>
|
||||
/// <param name="value">Giá trị cần ghi (true/false)</param>
|
||||
/// <returns>True nếu thành công, false nếu thất bại</returns>
|
||||
public async Task<bool> WriteCoil(string deviceId, ushort address, bool value)
|
||||
{
|
||||
var device = deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IModbusTcpDevice modbusDevice)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await modbusDevice.WriteCoilAsync(address, value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Map IModbusTcpDevice sang ModbusTcpDataDto
|
||||
/// </summary>
|
||||
private static ModbusTcpData MapToDto(IModbusTcpDevice modbusDevice)
|
||||
{
|
||||
var dto = new ModbusTcpData
|
||||
{
|
||||
IpAddress = modbusDevice.IpAddress,
|
||||
Port = modbusDevice.Port,
|
||||
SlaveId = modbusDevice.SlaveId,
|
||||
IsConnected = modbusDevice.IsConnected
|
||||
};
|
||||
|
||||
var holdingRegisters = new List<ModbusRangeData>();
|
||||
// Lấy Holding Registers
|
||||
foreach (var range in modbusDevice.HoldingRegisterRanges)
|
||||
{
|
||||
try
|
||||
{
|
||||
var values = modbusDevice.ReadHoldingRegisters(range.StartAddress, range.Quantity);
|
||||
var rangeData = new ModbusRangeData
|
||||
{
|
||||
StartAddress = range.StartAddress,
|
||||
Quantity = range.Quantity,
|
||||
Name = range.Name,
|
||||
ChildrenNames = range.ChildrenNames == null ? [] : [.. range.ChildrenNames]
|
||||
};
|
||||
|
||||
var modbusValues = new List<ModbusValue>();
|
||||
for (ushort i = 0; i < values.Length; i++)
|
||||
{
|
||||
var address = (ushort)(range.StartAddress + i);
|
||||
var childName = (range.ChildrenNames != null && i < range.ChildrenNames.Length) ? range.ChildrenNames[i] : "";
|
||||
modbusValues.Add(new ModbusValue
|
||||
{
|
||||
Address = address,
|
||||
Index = i,
|
||||
Value = values[i],
|
||||
Name = childName
|
||||
});
|
||||
}
|
||||
rangeData.Values = [.. modbusValues];
|
||||
holdingRegisters.Add(rangeData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip range nếu không đọc được
|
||||
}
|
||||
}
|
||||
dto.HoldingRegisters = [.. holdingRegisters];
|
||||
|
||||
|
||||
var inputRegisters = new List<ModbusRangeData>();
|
||||
// Lấy Input Registers
|
||||
foreach (var range in modbusDevice.InputRegisterRanges)
|
||||
{
|
||||
try
|
||||
{
|
||||
var values = modbusDevice.ReadInputRegisters(range.StartAddress, range.Quantity);
|
||||
var rangeData = new ModbusRangeData
|
||||
{
|
||||
StartAddress = range.StartAddress,
|
||||
Quantity = range.Quantity,
|
||||
Name = range.Name,
|
||||
ChildrenNames = range.ChildrenNames == null ? [] : [.. range.ChildrenNames]
|
||||
};
|
||||
|
||||
var modbusValues = new List<ModbusValue>();
|
||||
for (ushort i = 0; i < values.Length; i++)
|
||||
{
|
||||
var address = (ushort)(range.StartAddress + i);
|
||||
var childName = (range.ChildrenNames != null && i < range.ChildrenNames.Length) ? range.ChildrenNames[i] : "";
|
||||
modbusValues.Add(new ModbusValue
|
||||
{
|
||||
Address = address,
|
||||
Index = i,
|
||||
Value = values[i],
|
||||
Name = childName
|
||||
});
|
||||
}
|
||||
rangeData.Values = [.. modbusValues];
|
||||
|
||||
inputRegisters.Add(rangeData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip range nếu không đọc được
|
||||
}
|
||||
}
|
||||
dto.InputRegisters = [.. inputRegisters];
|
||||
|
||||
// Lấy Coils
|
||||
var coils = new List<ModbusRangeData>();
|
||||
foreach (var range in modbusDevice.CoilRanges)
|
||||
{
|
||||
try
|
||||
{
|
||||
var values = modbusDevice.ReadCoils(range.StartAddress, range.Quantity);
|
||||
var rangeData = new ModbusRangeData
|
||||
{
|
||||
StartAddress = range.StartAddress,
|
||||
Quantity = range.Quantity,
|
||||
Name = range.Name,
|
||||
ChildrenNames = range.ChildrenNames ?? []
|
||||
};
|
||||
|
||||
var modbusValues = new List<ModbusBoolValue>();
|
||||
for (ushort i = 0; i < values.Length; i++)
|
||||
{
|
||||
var address = (ushort)(range.StartAddress + i);
|
||||
var childName = (range.ChildrenNames != null && i < range.ChildrenNames.Length) ? range.ChildrenNames[i] : "";
|
||||
modbusValues.Add(new ModbusBoolValue
|
||||
{
|
||||
Address = address,
|
||||
Index = i,
|
||||
Value = values[i],
|
||||
Name = childName
|
||||
});
|
||||
}
|
||||
rangeData.BoolValues = [.. modbusValues];
|
||||
coils.Add(rangeData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip range nếu không đọc được
|
||||
}
|
||||
}
|
||||
dto.Coils = [.. coils];
|
||||
|
||||
// Lấy Discrete Inputs
|
||||
var inputs = new List<ModbusRangeData>();
|
||||
foreach (var range in modbusDevice.DiscreteInputRanges)
|
||||
{
|
||||
try
|
||||
{
|
||||
var values = modbusDevice.ReadDiscreteInputs(range.StartAddress, range.Quantity);
|
||||
var rangeData = new ModbusRangeData
|
||||
{
|
||||
StartAddress = range.StartAddress,
|
||||
Quantity = range.Quantity,
|
||||
Name = range.Name,
|
||||
ChildrenNames = range.ChildrenNames ?? []
|
||||
};
|
||||
|
||||
var modbusValues = new List<ModbusBoolValue>();
|
||||
for (ushort i = 0; i < values.Length; i++)
|
||||
{
|
||||
var address = (ushort)(range.StartAddress + i);
|
||||
var childName = (range.ChildrenNames != null && i < range.ChildrenNames.Length) ? range.ChildrenNames[i] : "";
|
||||
modbusValues.Add(new ModbusBoolValue
|
||||
{
|
||||
Address = address,
|
||||
Index = i,
|
||||
Value = values[i],
|
||||
Name = childName
|
||||
});
|
||||
}
|
||||
rangeData.BoolValues = [.. modbusValues];
|
||||
inputs.Add(rangeData);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Skip range nếu không đọc được
|
||||
}
|
||||
}
|
||||
dto.DiscreteInputs = [.. inputs];
|
||||
return dto;
|
||||
}
|
||||
}
|
||||
230
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/MotionHub.cs
Normal file
230
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/MotionHub.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Motion;
|
||||
using RobotNet10.RobotApp.Client.Shared.Modules;
|
||||
using RobotNet10.RobotApp.Motion;
|
||||
using RobotNet10.RobotApp.Modules;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub thống nhất cho tất cả các chức năng Motion:
|
||||
/// - ManualControl (điều khiển thủ công qua RF Handle)
|
||||
/// - Odometry (ước lượng vị trí robot)
|
||||
/// - LiftModule (điều khiển nâng hạ)
|
||||
/// - RotationModule (điều khiển xoay)
|
||||
/// </summary>
|
||||
[AllowAnonymous]
|
||||
public class MotionHub(
|
||||
ManualControlService manualControlService,
|
||||
IOdometryEstimator odometryEstimator,
|
||||
ILiftModule liftModule,
|
||||
IRotationModule rotationModule) : Hub
|
||||
{
|
||||
#region Manual Control
|
||||
|
||||
/// <summary>
|
||||
/// Lấy trạng thái hiện tại của ManualControlService
|
||||
/// </summary>
|
||||
public Task<ManualControlStatusDto> GetStatus()
|
||||
{
|
||||
var status = manualControlService.CurrentRfHandleStatus;
|
||||
var twist = manualControlService.CurrentTwist;
|
||||
|
||||
return Task.FromResult(new ManualControlStatusDto
|
||||
{
|
||||
State = manualControlService.State.ToString(),
|
||||
IsEnabled = manualControlService.State == ManualControlState.Maintenance ||
|
||||
manualControlService.State == ManualControlState.Override,
|
||||
CurrentLinearVelocity = twist.Linear.X,
|
||||
CurrentAngularVelocity = twist.Angular.Z,
|
||||
RfHandleStatus = status != null ? new RfHandleStatusDto
|
||||
{
|
||||
Heartbeat = status.Heartbeat,
|
||||
Ready = status.Ready,
|
||||
Locked = status.Locked,
|
||||
EStop = status.EStop,
|
||||
Enable = status.Enable,
|
||||
Speed = status.Speed,
|
||||
Linear = status.Linear,
|
||||
Angular = status.Angular,
|
||||
Mode = status.Mode,
|
||||
LastUpdateTime = status.LastUpdateTime
|
||||
} : null
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start manual control update loop
|
||||
/// </summary>
|
||||
public Task Enable()
|
||||
{
|
||||
manualControlService.Start();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop manual control update loop
|
||||
/// </summary>
|
||||
public Task Disable()
|
||||
{
|
||||
manualControlService.Stop();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Odometry
|
||||
|
||||
/// <summary>
|
||||
/// Lấy pose hiện tại của robot (odometry)
|
||||
/// </summary>
|
||||
public OdometryDto GetCurrentPose()
|
||||
{
|
||||
var pose = odometryEstimator.CurrentPose;
|
||||
var frequency = odometryEstimator.UpdateFrequency;
|
||||
return pose.ToOdometryDto(frequency);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reset odometry về vị trí ban đầu
|
||||
/// </summary>
|
||||
public void ResetOdometry() => odometryEstimator.ResetOdometry();
|
||||
|
||||
/// <summary>
|
||||
/// Reset odometry về một pose cụ thể
|
||||
/// </summary>
|
||||
public void ResetOdometryPose(OdometryDto dto)
|
||||
{
|
||||
if (dto != null)
|
||||
{
|
||||
odometryEstimator.ResetOdometry(dto.ToPose());
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Lift Module
|
||||
|
||||
/// <summary>
|
||||
/// Lấy trạng thái hiện tại của LiftModule
|
||||
/// </summary>
|
||||
public async Task<LiftModuleStatusDto> GetLiftStatus()
|
||||
{
|
||||
var position = await liftModule.GetCurrentPositionAsync();
|
||||
return new LiftModuleStatusDto
|
||||
{
|
||||
State = liftModule.State.ToString(),
|
||||
IsReady = liftModule.IsReady,
|
||||
CurrentPosition = position
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy vị trí hiện tại của Lift
|
||||
/// </summary>
|
||||
public async Task<int> GetLiftCurrentPosition()
|
||||
{
|
||||
return await liftModule.GetCurrentPositionAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Nâng lên
|
||||
/// </summary>
|
||||
public async Task LiftUp()
|
||||
{
|
||||
await liftModule.LiftUpAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hạ xuống
|
||||
/// </summary>
|
||||
public async Task LiftDown()
|
||||
{
|
||||
await liftModule.LiftDownAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dừng nâng/hạ (khi đang di chuyển)
|
||||
/// </summary>
|
||||
public async Task LiftStop()
|
||||
{
|
||||
await liftModule.LiftStopAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Di chuyển đến vị trí cụ thể
|
||||
/// </summary>
|
||||
public async Task LiftToPosition(int position)
|
||||
{
|
||||
await liftModule.LiftToPositionAsync(position);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra Lift module đã homed chưa
|
||||
/// </summary>
|
||||
public async Task<bool> IsLiftHomed()
|
||||
{
|
||||
return await liftModule.IsHomedAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thực hiện homing thủ công cho LiftModule
|
||||
/// </summary>
|
||||
public async Task LiftHome()
|
||||
{
|
||||
await liftModule.HomeAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Rotation Module
|
||||
|
||||
/// <summary>
|
||||
/// Lấy trạng thái hiện tại của RotationModule
|
||||
/// </summary>
|
||||
public async Task<RotationModuleStatusDto> GetRotationStatus()
|
||||
{
|
||||
var angle = await rotationModule.GetCurrentAngleAsync();
|
||||
return new RotationModuleStatusDto
|
||||
{
|
||||
State = rotationModule.State.ToString(),
|
||||
IsReady = rotationModule.IsReady,
|
||||
CurrentAngle = angle
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy góc hiện tại
|
||||
/// </summary>
|
||||
public async Task<double> GetRotationCurrentAngle()
|
||||
{
|
||||
return await rotationModule.GetCurrentAngleAsync();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xoay đến góc cụ thể (absolute)
|
||||
/// </summary>
|
||||
public async Task RotateToAngle(double angleDegrees)
|
||||
{
|
||||
await rotationModule.RotateToAngleAsync(angleDegrees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xoay một góc offset (relative)
|
||||
/// </summary>
|
||||
public async Task RotateOffset(double angleOffsetDegrees)
|
||||
{
|
||||
await rotationModule.RotateOffsetAsync(angleOffsetDegrees);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kiểm tra Rotation module đã homed chưa
|
||||
/// </summary>
|
||||
public async Task<bool> IsRotationHomed()
|
||||
{
|
||||
return await rotationModule.IsHomedAsync();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Services.NavigationMonitor;
|
||||
using RobotNet10.RobotApp.Shared.NavigationMonitor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class NavigationMonitorHub(NavigationMonitorService monitorService) : Hub
|
||||
{
|
||||
public async Task Subscribe()
|
||||
{
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, "monitor");
|
||||
}
|
||||
|
||||
public async Task Unsubscribe()
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, "monitor");
|
||||
}
|
||||
|
||||
public void SetTelemetryEnabled(bool enabled)
|
||||
{
|
||||
monitorService.SetTelemetryEnabled(enabled);
|
||||
}
|
||||
|
||||
public void SetSafetyStopEnabled(bool enabled)
|
||||
{
|
||||
monitorService.SetSafetyStopEnabled(enabled);
|
||||
}
|
||||
|
||||
public void UpdateSafetyConfig(NavigationSafetyConfigDto config)
|
||||
{
|
||||
monitorService.UpdateSafetyConfig(config);
|
||||
}
|
||||
|
||||
public void ReleaseSafetyStop()
|
||||
{
|
||||
monitorService.ReleaseSafetyStop();
|
||||
}
|
||||
|
||||
public NavigationMonitorStateDto GetState()
|
||||
{
|
||||
return monitorService.GetState();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using RobotNet10.RobotApp.Client.Shared.Motion;
|
||||
using RobotNet10.Shared.Geometry;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for OdometryDto conversions (server-side only)
|
||||
/// </summary>
|
||||
public static class OdometryDtoExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Convert from Pose to OdometryDto (compatibility path when only raw pose is available).
|
||||
/// </summary>
|
||||
public static OdometryDto ToOdometryDto(this Pose pose, double updateFrequency, string frameId = "odom")
|
||||
{
|
||||
return new OdometryDto
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
FrameId = frameId,
|
||||
PositionX = pose.Position.X,
|
||||
PositionY = pose.Position.Y,
|
||||
PositionZ = pose.Position.Z,
|
||||
OrientationX = pose.Orientation.X,
|
||||
OrientationY = pose.Orientation.Y,
|
||||
OrientationZ = pose.Orientation.Z,
|
||||
OrientationW = pose.Orientation.W,
|
||||
UpdateFrequency = updateFrequency
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert OdometryDto to Pose (for Reset operations)
|
||||
/// </summary>
|
||||
public static Pose ToPose(this OdometryDto dto)
|
||||
{
|
||||
return new Pose
|
||||
{
|
||||
Position = new Point(dto.PositionX, dto.PositionY, dto.PositionZ),
|
||||
Orientation = new Quaternion(dto.OrientationX, dto.OrientationY, dto.OrientationZ, dto.OrientationW)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Motion;
|
||||
using RobotNet10.RobotApp.Motion;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho Odometry - hiển thị pose và velocity từ OdometryService (wheel encoder + IMU).
|
||||
/// Cùng nguồn OdometryService.CurrentOdometry được XlocIntegrationService dùng để DispatchOdometry sang XLOC
|
||||
/// (khi EnableRawOdometry bật) và NavigationIntegrationService dùng để dispatch sang navigation.
|
||||
/// </summary>
|
||||
public class OdometryHub(OdometryService odometryService) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy odometry hiện tại (pose + velocity) để hiển thị trên UI
|
||||
/// </summary>
|
||||
public Task<OdometryDto?> GetCurrentOdometry()
|
||||
{
|
||||
var odom = odometryService.CurrentOdometry;
|
||||
var dto = MapToDto(odom);
|
||||
return Task.FromResult<OdometryDto?>(dto);
|
||||
}
|
||||
|
||||
private static OdometryDto MapToDto(Odometry odom)
|
||||
{
|
||||
var p = odom.Pose.Pose.Position;
|
||||
var q = odom.Pose.Pose.Orientation;
|
||||
var linear = odom.Twist.Twist.Linear;
|
||||
var angular = odom.Twist.Twist.Angular;
|
||||
|
||||
return new OdometryDto
|
||||
{
|
||||
Timestamp = odom.Header.Stamp,
|
||||
FrameId = odom.Header.FrameId ?? "odom",
|
||||
ChildFrameId = odom.ChildFrameId ?? "base_link",
|
||||
PositionX = p.X,
|
||||
PositionY = p.Y,
|
||||
PositionZ = p.Z,
|
||||
OrientationX = q.X,
|
||||
OrientationY = q.Y,
|
||||
OrientationZ = q.Z,
|
||||
OrientationW = q.W,
|
||||
LinearVelocityX = linear.X,
|
||||
LinearVelocityY = linear.Y,
|
||||
LinearVelocityZ = linear.Z,
|
||||
AngularVelocityX = angular.X,
|
||||
AngularVelocityY = angular.Y,
|
||||
AngularVelocityZ = angular.Z,
|
||||
UpdateFrequency = 0 // Caller can compute if needed
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Motion;
|
||||
using RobotNet10.RobotApp.Motion;
|
||||
using RobotNet10.Shared.Sensor;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// HubContext để broadcast odometry đến tất cả clients (realtime hiển thị odom)
|
||||
/// </summary>
|
||||
public class OdometryHubContext
|
||||
{
|
||||
private readonly IHubContext<OdometryHub> _hubContext;
|
||||
private readonly OdometryService _odometryService;
|
||||
|
||||
public OdometryHubContext(IHubContext<OdometryHub> hubContext, OdometryService odometryService)
|
||||
{
|
||||
_hubContext = hubContext;
|
||||
_odometryService = odometryService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Broadcast odometry hiện tại đến tất cả clients
|
||||
/// </summary>
|
||||
public async Task BroadcastOdometryAsync()
|
||||
{
|
||||
var odom = _odometryService.CurrentOdometry;
|
||||
var dto = MapToDto(odom);
|
||||
await _hubContext.Clients.All.SendAsync("ReceiveOdometry", dto);
|
||||
}
|
||||
|
||||
private static OdometryDto MapToDto(Odometry odom)
|
||||
{
|
||||
var p = odom.Pose.Pose.Position;
|
||||
var q = odom.Pose.Pose.Orientation;
|
||||
var linear = odom.Twist.Twist.Linear;
|
||||
var angular = odom.Twist.Twist.Angular;
|
||||
|
||||
return new OdometryDto
|
||||
{
|
||||
Timestamp = odom.Header.Stamp,
|
||||
FrameId = odom.Header.FrameId ?? "odom",
|
||||
ChildFrameId = odom.ChildFrameId ?? "base_link",
|
||||
PositionX = p.X,
|
||||
PositionY = p.Y,
|
||||
PositionZ = p.Z,
|
||||
OrientationX = q.X,
|
||||
OrientationY = q.Y,
|
||||
OrientationZ = q.Z,
|
||||
OrientationW = q.W,
|
||||
LinearVelocityX = linear.X,
|
||||
LinearVelocityY = linear.Y,
|
||||
LinearVelocityZ = linear.Z,
|
||||
AngularVelocityX = angular.X,
|
||||
AngularVelocityY = angular.Y,
|
||||
AngularVelocityZ = angular.Z,
|
||||
UpdateFrequency = 0
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Plc;
|
||||
using RobotNet10.RobotApp.Interfaces;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho PlcController
|
||||
/// Cung cấp real-time updates về trạng thái PLC (read-only)
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class PlcControllerHub(IPlcController plcController) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Lấy trạng thái hiện tại của PlcController
|
||||
/// </summary>
|
||||
public Task<PlcControllerStatusDto> GetStatus()
|
||||
{
|
||||
return Task.FromResult(new PlcControllerStatusDto
|
||||
{
|
||||
IsReady = plcController.IsReady,
|
||||
|
||||
// Operating mode
|
||||
PeripheralMode = plcController.PeripheralMode.ToString(),
|
||||
SafetySpeed = plcController.SafetySpeed.ToString(),
|
||||
|
||||
// Safety sensors
|
||||
Emergency = plcController.Emergency,
|
||||
Bumper = plcController.Bumper,
|
||||
LidarFrontProtectField = plcController.LidarFrontProtectField,
|
||||
LidarBackProtectField = plcController.LidarBackProtectField,
|
||||
LidarFrontTimProtectField = plcController.LidarFrontTimProtectField,
|
||||
|
||||
// Lift state
|
||||
LiftedUp = plcController.LiftedUp,
|
||||
LiftedDown = plcController.LiftedDown,
|
||||
LiftHome = plcController.LiftHome,
|
||||
|
||||
// Motor state
|
||||
LeftMotorReady = plcController.LeftMotorReady,
|
||||
RightMotorReady = plcController.RightMotorReady,
|
||||
LiftMotorReady = plcController.LiftMotorReady,
|
||||
|
||||
// Button state
|
||||
ButtonStart = plcController.ButtonStart,
|
||||
ButtonStop = plcController.ButtonStop,
|
||||
ButtonReset = plcController.ButtonReset,
|
||||
|
||||
// Other state
|
||||
HasLoad = plcController.HasLoad,
|
||||
EnabledCharger = plcController.EnabledCharger,
|
||||
Charging = plcController.Charging,
|
||||
MutedBase = plcController.MutedBase,
|
||||
MutedLoad = plcController.MutedLoad,
|
||||
|
||||
// Determine current stop state
|
||||
StopState = GetStopState(),
|
||||
|
||||
// Write state - các giá trị đã được ghi xuống PLC
|
||||
CurrentSystemState = plcController.CurrentSystemState.ToString(),
|
||||
CurrentOperationState = plcController.CurrentOperationState.ToString(),
|
||||
CurrentRFMode = plcController.CurrentRFMode.ToString(),
|
||||
SetHorizontalLoadValue = plcController.SetHorizontalLoadValue,
|
||||
SetMutedBaseValue = plcController.SetMutedBaseValue,
|
||||
SetMutedLoadValue = plcController.SetMutedLoadValue,
|
||||
SetEnableChargerValue = plcController.SetEnableChargerValue,
|
||||
SetHasLoadValue = plcController.SetHasLoadValue,
|
||||
SetRFEStopValue = plcController.SetRFEStopValue,
|
||||
SetBatteryLowValue = plcController.SetBatteryLowValue,
|
||||
SetLightOnValue = plcController.SetLightOnValue
|
||||
});
|
||||
}
|
||||
|
||||
private string GetStopState()
|
||||
{
|
||||
if (plcController.Emergency) return "EMC";
|
||||
if (plcController.Bumper) return "Bumper";
|
||||
if (!plcController.LidarFrontProtectField) return "FrontProtective";
|
||||
if (!plcController.LidarBackProtectField) return "BackProtective";
|
||||
if (plcController.LidarFrontTimProtectField) return "TimProtective";
|
||||
return "None";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.Devices;
|
||||
using RobotNet10.RobotApp.Devices;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
[Authorize]
|
||||
public class RfHandleHub(IDeviceProvider _deviceProvider) : Hub
|
||||
{
|
||||
// Snapshot / RPC calls
|
||||
public Task<DeviceInfoDto?> GetDeviceInfo(string deviceId)
|
||||
{
|
||||
var device = _deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IRfHandle)
|
||||
return Task.FromResult<DeviceInfoDto?>(null);
|
||||
|
||||
return Task.FromResult<DeviceInfoDto?>(new DeviceInfoDto
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
DeviceName = device.DeviceName
|
||||
});
|
||||
}
|
||||
|
||||
public Task<RfHandleDataDto?> GetRfHandleData(string deviceId)
|
||||
{
|
||||
var device = _deviceProvider.GetDevice(deviceId);
|
||||
if (device is not IRfHandle rf)
|
||||
return Task.FromResult<RfHandleDataDto?>(null);
|
||||
|
||||
var dto = MapToDto(rf, deviceId);
|
||||
return Task.FromResult<RfHandleDataDto?>(dto);
|
||||
}
|
||||
|
||||
// ====== Helper & DTO mapper ======
|
||||
private static RfHandleDataDto MapToDto(IRfHandle rf, string deviceId)
|
||||
{
|
||||
return new RfHandleDataDto
|
||||
{
|
||||
DeviceId = deviceId,
|
||||
Heartbeat = rf.Heartbeat,
|
||||
RemoteReady = rf.RemoteReady,
|
||||
EStop = rf.EStop,
|
||||
|
||||
LiftUp = rf.LiftUp,
|
||||
LiftDown = rf.LiftDown,
|
||||
RotateLeft = rf.RotateLeft,
|
||||
RotateRight = rf.RotateRight,
|
||||
|
||||
ModeSelect = rf.ModeSelect,
|
||||
Enable = rf.Enable,
|
||||
|
||||
Speed = rf.Speed,
|
||||
Linear = rf.Linear,
|
||||
Angular = rf.Angular,
|
||||
Mode = rf.Mode.ToString(),
|
||||
|
||||
LastUpdateTime = rf.LastUpdateTime
|
||||
};
|
||||
}
|
||||
}
|
||||
476
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/SLAMHub.cs
Normal file
476
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/SLAMHub.cs
Normal file
@@ -0,0 +1,476 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using RobotNet10.RobotApp.Client.Shared.SLAM;
|
||||
using RobotNet10.RobotApp.Shared;
|
||||
using RobotNet10.RobotApp.Shared.Enums;
|
||||
using RobotNet10.RobotApp.SLAM;
|
||||
using RobotNet10.RobotApp.SLAM.Cartographer.Geometry;
|
||||
using RobotNet10.Shared.Geometry;
|
||||
using RobotNet10.Shared.Localization;
|
||||
using RobotNet10.Shared.Numbers;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub cho Cartographer/Localization - cung cấp real-time mapping và localization data
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class SLAMHub(ISLAMService _slamService, ILogger<SLAMHub> _logger) : Hub
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a client connects to the hub
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
await base.OnConnectedAsync();
|
||||
// Clients will poll for occupancy grid using GetOccupancyGridBase() and GetOccupancyGridUpdating() methods
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a client disconnects from the hub
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
/// <summary>
|
||||
/// Lấy trạng thái hiện tại của CartographerService
|
||||
/// </summary>
|
||||
public Task<SLAMState> GetCurrentState()
|
||||
{
|
||||
var state = _slamService.State;
|
||||
return Task.FromResult(MapState(state));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy tên map hiện tại đang được sử dụng
|
||||
/// </summary>
|
||||
public Task<string?> GetCurrentMap()
|
||||
{
|
||||
return Task.FromResult(_slamService.CurrentMap);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bắt đầu localization với map đã lưu
|
||||
/// </summary>
|
||||
public async Task<bool> StartLocalization(string mapName, PoseDto? initialPoseDto = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Pose? initialPose = initialPoseDto != null ? MapPoseFromDto(initialPoseDto) : null;
|
||||
_slamService.StartLocalization(mapName, initialPose);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to start localization");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dừng localization
|
||||
/// </summary>
|
||||
public Task StopLocalization()
|
||||
{
|
||||
_slamService.StopLocalization();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bắt đầu scan & mapping
|
||||
/// </summary>
|
||||
public async Task<bool> StartScanMapping(string mapName)
|
||||
{
|
||||
try
|
||||
{
|
||||
_slamService.StartScanMapping(mapName);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to start scan mapping");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lưu map hiện tại.
|
||||
/// Note: This is now fire-and-forget. The actual save happens asynchronously.
|
||||
/// Monitor state changes to know when save completes.
|
||||
/// </summary>
|
||||
public async Task<string?> SaveMap()
|
||||
{
|
||||
try
|
||||
{
|
||||
_slamService.SaveScanMap();
|
||||
// Return null since we no longer wait for completion
|
||||
// Clients should monitor state changes instead
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to save map");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Liệt kê tất cả maps có sẵn
|
||||
/// </summary>
|
||||
public MapInfoDto[] ListMaps()
|
||||
{
|
||||
try
|
||||
{
|
||||
var maps = _slamService.ListMaps();
|
||||
return [.. maps.Select(MapMapInfoToDto)];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to list maps");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy thông tin map và đăng ký client vào group để nhận cập nhật trạng thái xử lý
|
||||
/// </summary>
|
||||
/// <param name="mapName">Tên map</param>
|
||||
/// <returns>MapInfoDto với trạng thái IsProcessing</returns>
|
||||
public async Task<MapInfoDto?> GetMapInfoAndSubscribeProcessing(string mapName)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Subscribe client to group with mapName
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, mapName);
|
||||
|
||||
// Get map info
|
||||
var mapInfo = _slamService.GetMapInfo(mapName);
|
||||
if (mapInfo == null)
|
||||
{
|
||||
_logger.LogWarning("SLAMHub: Map not found: {MapName}", mapName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get processing status
|
||||
var isProcessing = _slamService.GetMapProcessingStatus(mapName);
|
||||
|
||||
// Map to DTO with IsProcessing
|
||||
var dto = MapMapInfoToDto(mapInfo);
|
||||
dto.IsProcessing = isProcessing;
|
||||
|
||||
return dto;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SLAMHub: Failed to get map info and subscribe: {MapName}", mapName);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Hủy đăng ký client khỏi group nhận cập nhật trạng thái xử lý
|
||||
/// </summary>
|
||||
/// <param name="mapName">Tên map</param>
|
||||
public async Task UnsubscribeMapProcessing(string mapName)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, mapName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SLAMHub: Failed to unsubscribe from map processing: {MapName}", mapName);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Xóa map
|
||||
/// </summary>
|
||||
public async Task<bool> DeleteMap(string mapName)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _slamService.DeleteMapAsync(mapName);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to delete map");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform map origin to a new pose
|
||||
/// </summary>
|
||||
/// <param name="mapName">Name of the map to transform</param>
|
||||
/// <param name="newOriginDto">New origin pose (position and orientation)</param>
|
||||
/// <returns>True if transform was successful</returns>
|
||||
public async Task<bool> TransformMapOrigin(string mapName, PoseDto newOriginDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newOrigin = MapPoseFromDto(newOriginDto);
|
||||
return await _slamService.TransformMapOriginAsync(mapName, newOrigin);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to transform map origin");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rerender map image files (PNG, JPG, PGM) with custom OccupancyGridConfiguration.
|
||||
/// This is an async operation - returns true if processing started successfully.
|
||||
/// Clients subscribed to the map group will receive OnMapProcessingChanged notifications.
|
||||
/// </summary>
|
||||
/// <param name="mapName">Name of the map to rerender</param>
|
||||
/// <param name="configDto">Custom occupancy grid configuration</param>
|
||||
/// <returns>True if rerender was started successfully</returns>
|
||||
public async Task<bool> RerenderMapWithConfig(string mapName, OccupancyGridConfigurationDto configDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _slamService.RerenderMapWithConfigAsync(mapName, configDto);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "SLAMHub: Failed to rerender map with config");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set initial pose cho localization
|
||||
/// </summary>
|
||||
public async Task SetInitialPose(PoseDto poseDto)
|
||||
{
|
||||
try
|
||||
{
|
||||
var pose = MapPoseFromDto(poseDto);
|
||||
_slamService.SetInitialPose(pose);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "CartographerHub: Failed to set initial pose");
|
||||
await Clients.Caller.SendAsync("OnError", new ErrorDto
|
||||
{
|
||||
Message = ex.Message,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy occupancy grid mới hơn thời gian chỉ định
|
||||
/// </summary>
|
||||
/// <param name="since">Thời gian để so sánh</param>
|
||||
/// <returns>OccupancyGridDto nếu có update mới hơn, null nếu không có</returns>
|
||||
public OccupancyGridDto? GetOccupancyGrid(DateTime since)
|
||||
{
|
||||
var lastUpdated = _slamService.LastUpdatedOccupancyGrid;
|
||||
var grid = _slamService.GetOccupancyGrid(since);
|
||||
var trajectoryNodes = _slamService.GetTrajectoryNodes();
|
||||
|
||||
if (grid == null)
|
||||
{
|
||||
_logger.LogDebug(
|
||||
"SLAMHub.GetOccupancyGrid: No grid available (since={Since}, lastUpdated={LastUpdated}, state={State})",
|
||||
since, lastUpdated, _slamService.State);
|
||||
return null;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"SLAMHub.GetOccupancyGrid: Returning grid {W}x{H}, lastUpdated={LastUpdated}",
|
||||
grid.Width, grid.Height, lastUpdated);
|
||||
|
||||
return MapOccupancyGridToDto(grid, lastUpdated, lastUpdated, versionTicks: lastUpdated.Ticks, trajectoryNodes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy pose hiện tại của robot
|
||||
/// Trả về pose từ ScanMappingService nếu đang ScanMapping, hoặc từ CartographerService nếu đang Localizing
|
||||
/// </summary>
|
||||
/// <returns>PoseDto nếu có pose, null nếu không có</returns>
|
||||
public PoseDto GetCurrentPose()
|
||||
{
|
||||
var state = _slamService.State;
|
||||
|
||||
// Khi đang ScanMapping, lấy pose từ ScanMappingService
|
||||
if (state == SLAMState.ScanMapping)
|
||||
{
|
||||
return MapPoseToDto(_slamService.CurrentPose, 0);
|
||||
}
|
||||
// Khi đang Localizing, lấy pose từ CartographerService
|
||||
else if (state == SLAMState.Localizing)
|
||||
{
|
||||
return MapPoseToDto(_slamService.CurrentPose, _slamService.LocalizationScore ?? 0, _slamService.PoseCovariance);
|
||||
}
|
||||
|
||||
return MapPoseToDto(new Pose(), 0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lấy sample point cloud từ tất cả lidar devices (đã transform sang pose-graph global frame).
|
||||
/// Pipeline: LocalTrajectoryBuilder2D → trajectory-with-global-orientation, GlobalTrajectoryBuilder2D → GetLocalToGlobalTransform → global.
|
||||
/// </summary>
|
||||
/// <returns>Danh sách Point32 trong global (map) frame, empty list nếu không có</returns>
|
||||
public Vector3[] GetSamplePointCloud()
|
||||
{
|
||||
var points = _slamService.GetAggregatedSamplePointCloud();
|
||||
return points?.Count > 0 ? [.. points] : [];
|
||||
}
|
||||
|
||||
// Mapping methods (convert server types to DTOs)
|
||||
|
||||
private static SLAMState MapState(SLAMState state)
|
||||
{
|
||||
return state switch
|
||||
{
|
||||
SLAMState.Idle => SLAMState.Idle,
|
||||
SLAMState.Initializing => SLAMState.Initializing,
|
||||
SLAMState.Ready => SLAMState.Ready,
|
||||
SLAMState.Relocalizing => SLAMState.Relocalizing,
|
||||
SLAMState.Localizing => SLAMState.Localizing,
|
||||
SLAMState.ScanMapping => SLAMState.ScanMapping,
|
||||
SLAMState.SavingMap => SLAMState.SavingMap,
|
||||
SLAMState.Error => SLAMState.Error,
|
||||
_ => SLAMState.Idle
|
||||
};
|
||||
}
|
||||
|
||||
private static Pose MapPoseFromDto(PoseDto dto)
|
||||
{
|
||||
return new Pose
|
||||
{
|
||||
Position = dto.Position,
|
||||
Orientation = dto.Orientation
|
||||
};
|
||||
}
|
||||
|
||||
private static PoseDto MapPoseToDto(Pose pose, double score, Matrix3x3? covariance = null)
|
||||
{
|
||||
var dto = new PoseDto
|
||||
{
|
||||
Position = pose.Position,
|
||||
Orientation = pose.Orientation,
|
||||
Score = score,
|
||||
Timestamp = DateTime.UtcNow
|
||||
};
|
||||
|
||||
// Convert covariance matrix to flattened array if available
|
||||
if (covariance.HasValue)
|
||||
{
|
||||
var cov = covariance.Value;
|
||||
// Matrix3x3 uses indexer [row, col] where:
|
||||
// row 0 = x, row 1 = y, row 2 = theta
|
||||
// col 0 = x, col 1 = y, col 2 = theta
|
||||
dto.Covariance =
|
||||
[
|
||||
cov[0, 0], cov[0, 1], cov[0, 2], // XX, XY, XT
|
||||
cov[1, 0], cov[1, 1], cov[1, 2], // YX, YY, YT
|
||||
cov[2, 0], cov[2, 1], cov[2, 2] // TX, TY, TT
|
||||
];
|
||||
}
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private OccupancyGridDto MapOccupancyGridToDto(OccupancyGrid grid, DateTime lastBaseUpdated, DateTime lastUpdated, long versionTicks, List<(int NodeId, Pose Pose)>? trajectoryNodes = null)
|
||||
{
|
||||
if (grid == null)
|
||||
{
|
||||
_logger.LogWarning("CartographerHub: MapOccupancyGridToDto called with null grid");
|
||||
throw new ArgumentNullException(nameof(grid));
|
||||
}
|
||||
|
||||
// Convert to sparse format: only include known cells (value >= 0)
|
||||
// Format: byte[] với mỗi cell = 5 bytes: [index_byte0, index_byte1, index_byte2, index_byte3, value]
|
||||
// Index: 4 bytes little-endian (32-bit unsigned integer)
|
||||
// Value: 1 byte (0-100)
|
||||
var knownCells = new List<byte>();
|
||||
|
||||
int knownCellCount = 0;
|
||||
for (int i = 0; i < grid.Data.Length; i++)
|
||||
{
|
||||
var value = grid.Data[i];
|
||||
if (value >= 0) // Only include known cells (skip unknown = -1)
|
||||
{
|
||||
knownCellCount++;
|
||||
// Encode index as 4 bytes (little-endian)
|
||||
knownCells.Add((byte)(i & 0xFF)); // Byte 0: LSB
|
||||
knownCells.Add((byte)((i >> 8) & 0xFF)); // Byte 1
|
||||
knownCells.Add((byte)((i >> 16) & 0xFF)); // Byte 2
|
||||
knownCells.Add((byte)((i >> 24) & 0xFF)); // Byte 3: MSB
|
||||
|
||||
// Encode value as 1 byte (0-100)
|
||||
knownCells.Add((byte)value);
|
||||
}
|
||||
}
|
||||
|
||||
TrajectoryNodeDto[]? trajectoryDtos = null;
|
||||
if (trajectoryNodes != null && trajectoryNodes.Count > 0)
|
||||
{
|
||||
trajectoryDtos = [.. trajectoryNodes.Select(node => new TrajectoryNodeDto
|
||||
{
|
||||
NodeId = node.NodeId,
|
||||
Pose = node.Pose,
|
||||
Timestamp = DateTime.UtcNow
|
||||
})];
|
||||
}
|
||||
|
||||
return new OccupancyGridDto
|
||||
{
|
||||
Resolution = grid.Resolution,
|
||||
Width = grid.Width,
|
||||
Height = grid.Height,
|
||||
Origin = grid.Origin,
|
||||
KnownCells = [.. knownCells],
|
||||
Version = versionTicks,
|
||||
LastBaseUpdated = lastBaseUpdated,
|
||||
LastUpdated = lastUpdated,
|
||||
TrajectoryNodes = trajectoryDtos
|
||||
};
|
||||
}
|
||||
|
||||
private static MapInfoDto MapMapInfoToDto(MapInfo mapInfo)
|
||||
{
|
||||
return new MapInfoDto
|
||||
{
|
||||
Name = mapInfo.Name,
|
||||
CreatedDate = mapInfo.CreatedDate,
|
||||
Resolution = mapInfo.Resolution,
|
||||
Width = mapInfo.Size.Width,
|
||||
Height = mapInfo.Size.Height,
|
||||
TrajectoryNodeCount = mapInfo.TrajectoryNodeCount,
|
||||
OriginX = mapInfo.Origin.Position.X,
|
||||
OriginY = mapInfo.Origin.Position.Y,
|
||||
};
|
||||
}
|
||||
}
|
||||
13
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/XlocHub.cs
Normal file
13
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/XlocHub.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub for broadcasting XLOC real-time data
|
||||
/// Clients can subscribe to pose and diagnostics updates
|
||||
/// </summary>
|
||||
public class XlocHub : Hub
|
||||
{
|
||||
// Hub methods for client-to-server communication can be added here if needed
|
||||
// Currently this hub is used only for server-to-client broadcasting
|
||||
}
|
||||
273
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/XlocPoseHub.cs
Normal file
273
srcs/RobotNet10/RobotApp/RobotNet10.RobotApp/Hubs/XlocPoseHub.cs
Normal file
@@ -0,0 +1,273 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace RobotNet10.RobotApp.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub for streaming XLOC pose data in realtime
|
||||
/// AND controlling SLAM operations manually (start/stop localization/mapping)
|
||||
/// </summary>
|
||||
public class XlocPoseHub : Hub
|
||||
{
|
||||
private readonly ILogger<XlocPoseHub> _logger;
|
||||
private readonly Xloc.XlocIntegrationService? _xlocService;
|
||||
|
||||
public XlocPoseHub(ILogger<XlocPoseHub> logger, Xloc.XlocIntegrationService? xlocService = null)
|
||||
{
|
||||
_logger = logger;
|
||||
_xlocService = xlocService;
|
||||
}
|
||||
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
_logger.LogInformation("Client connected to XlocPoseHub: {ConnectionId}", Context.ConnectionId);
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
_logger.LogInformation("Client disconnected from XlocPoseHub: {ConnectionId}", Context.ConnectionId);
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Client requests current pose (on-demand)
|
||||
/// </summary>
|
||||
public async Task RequestCurrentPose()
|
||||
{
|
||||
_logger.LogDebug("Client {ConnectionId} requested current pose", Context.ConnectionId);
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
#region Manual SLAM Control Methods
|
||||
|
||||
/// <summary>
|
||||
/// Activate a map for localization
|
||||
/// </summary>
|
||||
public async Task<bool> ActivateMap(string mapFilePath)
|
||||
{
|
||||
if (_xlocService == null)
|
||||
{
|
||||
_logger.LogWarning("XlocIntegrationService not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Client {ConnectionId} activating map: {MapPath}", Context.ConnectionId, mapFilePath);
|
||||
return _xlocService.ActivateMap(mapFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start localization mode
|
||||
/// </summary>
|
||||
public async Task<bool> StartLocalization()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
{
|
||||
_logger.LogWarning("XlocIntegrationService not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Client {ConnectionId} starting localization", Context.ConnectionId);
|
||||
return _xlocService.StartLocalization();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop localization mode
|
||||
/// </summary>
|
||||
public async Task<bool> StopLocalization()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
{
|
||||
_logger.LogWarning("XlocIntegrationService not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Client {ConnectionId} stopping localization", Context.ConnectionId);
|
||||
return _xlocService.StopLocalization();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Start mapping mode
|
||||
/// </summary>
|
||||
public async Task<bool> StartMapping()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
{
|
||||
_logger.LogWarning("XlocIntegrationService not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Client {ConnectionId} starting mapping", Context.ConnectionId);
|
||||
return _xlocService.StartMapping();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stop mapping and save to file
|
||||
/// </summary>
|
||||
public async Task<bool> StopMapping(string saveMapFilePath)
|
||||
{
|
||||
if (_xlocService == null)
|
||||
{
|
||||
_logger.LogWarning("XlocIntegrationService not available");
|
||||
return false;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Client {ConnectionId} stopping mapping, saving to: {MapPath}",
|
||||
Context.ConnectionId, saveMapFilePath);
|
||||
return _xlocService.StopMapping(saveMapFilePath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get current 2D pose (x, y, yaw)
|
||||
/// </summary>
|
||||
public async Task<object?> GetCurrentPose2D()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
return null;
|
||||
|
||||
var pose = _xlocService.GetCurrentPose2D();
|
||||
if (!pose.HasValue)
|
||||
return null;
|
||||
|
||||
var (x, y, yaw) = pose.Value;
|
||||
return new { x, y, yaw, yawDegrees = yaw * 180.0 / Math.PI };
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get sampled laser scan data (1 degree intervals) for web visualization
|
||||
/// Returns array of {angle, range} objects
|
||||
/// </summary>
|
||||
public async Task<object?> GetSampledLaserScan()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
return null;
|
||||
|
||||
var laserData = _xlocService.GetSampledLaserScan();
|
||||
if (laserData == null || laserData.Count == 0)
|
||||
return null;
|
||||
|
||||
// Convert to JSON-friendly format
|
||||
var points = laserData.Select(p => new { angle = p.angle, range = p.range }).ToArray();
|
||||
|
||||
return new {
|
||||
pointCount = points.Length,
|
||||
points = points,
|
||||
timestamp = DateTime.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get XLOC diagnostics data
|
||||
/// </summary>
|
||||
public async Task<object?> GetDiagnostics()
|
||||
{
|
||||
if (_xlocService == null)
|
||||
return null;
|
||||
|
||||
var diagnostics = _xlocService.GetDiagnostics();
|
||||
if (diagnostics == null)
|
||||
return null;
|
||||
|
||||
return new
|
||||
{
|
||||
header = new
|
||||
{
|
||||
seq = diagnostics.HeaderSeq,
|
||||
stamp = new
|
||||
{
|
||||
sec = diagnostics.HeaderStampSec,
|
||||
nsec = diagnostics.HeaderStampNsec
|
||||
},
|
||||
frameId = diagnostics.HeaderFrameId
|
||||
},
|
||||
xlocState = diagnostics.XlocState,
|
||||
stateString = diagnostics.StateString,
|
||||
currentActiveMap = diagnostics.CurrentActiveMap,
|
||||
reliability = diagnostics.Reliability,
|
||||
matchingScore = diagnostics.MatchingScore
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pose data for SignalR clients
|
||||
/// </summary>
|
||||
public class XlocPoseData
|
||||
{
|
||||
/// <summary>
|
||||
/// X position in meters
|
||||
/// </summary>
|
||||
public double X { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Y position in meters
|
||||
/// </summary>
|
||||
public double Y { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Z position in meters
|
||||
/// </summary>
|
||||
public double Z { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Yaw angle in radians
|
||||
/// </summary>
|
||||
public double Yaw { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Yaw angle in degrees
|
||||
/// </summary>
|
||||
public double YawDegrees => Yaw * 180.0 / Math.PI;
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion X
|
||||
/// </summary>
|
||||
public double QuaternionX { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion Y
|
||||
/// </summary>
|
||||
public double QuaternionY { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion Z
|
||||
/// </summary>
|
||||
public double QuaternionZ { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Quaternion W
|
||||
/// </summary>
|
||||
public double QuaternionW { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp of pose
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Is localization active
|
||||
/// </summary>
|
||||
public bool IsActive { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Xloc state (0=MAPPING, 1=LOCALIZATION, 2=PROCESSING, 3=READY, 4=ERROR)
|
||||
/// </summary>
|
||||
public byte XlocState { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Current active map name
|
||||
/// </summary>
|
||||
public string MapName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Localization reliability (0.0 to 1.0)
|
||||
/// </summary>
|
||||
public double Reliability { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// SLAM matching score
|
||||
/// </summary>
|
||||
public double MatchingScore { get; set; }
|
||||
}
|
||||
Reference in New Issue
Block a user