using Microsoft.AspNetCore.SignalR; using System.Threading.Tasks; namespace RobotNet10.RobotApp.Hubs; /// /// SignalR Hub for streaming XLOC pose data in realtime /// AND controlling SLAM operations manually (start/stop localization/mapping) /// public class XlocPoseHub : Hub { private readonly ILogger _logger; private readonly Xloc.XlocIntegrationService? _xlocService; public XlocPoseHub(ILogger 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); } /// /// Client requests current pose (on-demand) /// public async Task RequestCurrentPose() { _logger.LogDebug("Client {ConnectionId} requested current pose", Context.ConnectionId); await Task.CompletedTask; } #region Manual SLAM Control Methods /// /// Activate a map for localization /// public async Task 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); } /// /// Start localization mode /// public async Task StartLocalization() { if (_xlocService == null) { _logger.LogWarning("XlocIntegrationService not available"); return false; } _logger.LogInformation("Client {ConnectionId} starting localization", Context.ConnectionId); return _xlocService.StartLocalization(); } /// /// Stop localization mode /// public async Task StopLocalization() { if (_xlocService == null) { _logger.LogWarning("XlocIntegrationService not available"); return false; } _logger.LogInformation("Client {ConnectionId} stopping localization", Context.ConnectionId); return _xlocService.StopLocalization(); } /// /// Start mapping mode /// public async Task StartMapping() { if (_xlocService == null) { _logger.LogWarning("XlocIntegrationService not available"); return false; } _logger.LogInformation("Client {ConnectionId} starting mapping", Context.ConnectionId); return _xlocService.StartMapping(); } /// /// Stop mapping and save to file /// public async Task 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); } /// /// Get current 2D pose (x, y, yaw) /// public async Task 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 }; } /// /// Get sampled laser scan data (1 degree intervals) for web visualization /// Returns array of {angle, range} objects /// public async Task 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 }; } /// /// Get XLOC diagnostics data /// public async Task 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 } /// /// Pose data for SignalR clients /// public class XlocPoseData { /// /// X position in meters /// public double X { get; set; } /// /// Y position in meters /// public double Y { get; set; } /// /// Z position in meters /// public double Z { get; set; } /// /// Yaw angle in radians /// public double Yaw { get; set; } /// /// Yaw angle in degrees /// public double YawDegrees => Yaw * 180.0 / Math.PI; /// /// Quaternion X /// public double QuaternionX { get; set; } /// /// Quaternion Y /// public double QuaternionY { get; set; } /// /// Quaternion Z /// public double QuaternionZ { get; set; } /// /// Quaternion W /// public double QuaternionW { get; set; } /// /// Timestamp of pose /// public DateTime Timestamp { get; set; } /// /// Is localization active /// public bool IsActive { get; set; } /// /// Xloc state (0=MAPPING, 1=LOCALIZATION, 2=PROCESSING, 3=READY, 4=ERROR) /// public byte XlocState { get; set; } /// /// Current active map name /// public string MapName { get; set; } = string.Empty; /// /// Localization reliability (0.0 to 1.0) /// public double Reliability { get; set; } /// /// SLAM matching score /// public double MatchingScore { get; set; } }