Initial commit
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
using System.Net.Http.Json;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Responses;
|
||||
|
||||
namespace RobotNet10.FleetManager.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// API service for robot operations.
|
||||
/// Provides methods to interact with the robot API endpoints.
|
||||
/// </summary>
|
||||
public class RobotApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<RobotApiService>? _logger;
|
||||
|
||||
public RobotApiService(HttpClient httpClient, ILogger<RobotApiService>? logger = null)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all robots with optional filters
|
||||
/// </summary>
|
||||
public async Task<List<RobotDto>> GetAllAsync(Guid? modelId = null, Guid? mapId = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var queryParams = new List<string>();
|
||||
if (modelId.HasValue)
|
||||
queryParams.Add($"modelId={modelId.Value}");
|
||||
if (mapId.HasValue)
|
||||
queryParams.Add($"mapId={mapId.Value}");
|
||||
|
||||
var queryString = queryParams.Count > 0 ? "?" + string.Join("&", queryParams) : "";
|
||||
var response = await _httpClient.GetAsync($"/api/robots{queryString}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<RobotDto>>() ?? new List<RobotDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting all robots");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot by ID
|
||||
/// </summary>
|
||||
public async Task<RobotDto?> GetByIdAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robots/{id}");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting robot {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot by RobotId (string identifier)
|
||||
/// </summary>
|
||||
public async Task<RobotDto?> GetByRobotIdAsync(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robots/robotId/{Uri.EscapeDataString(robotId)}");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting robot with RobotId {RobotId}", robotId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search robots
|
||||
/// </summary>
|
||||
public async Task<List<RobotDto>> SearchAsync(string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robots/search?query={Uri.EscapeDataString(query)}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<RobotDto>>() ?? new List<RobotDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error searching robots with query {Query}", query);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all robots by model ID
|
||||
/// </summary>
|
||||
public async Task<List<RobotDto>> GetByModelIdAsync(Guid modelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robots/model/{modelId}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<RobotDto>>() ?? new List<RobotDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting robots by model {ModelId}", modelId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new robot
|
||||
/// </summary>
|
||||
public async Task<RobotDto> CreateAsync(CreateRobotRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/robots", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotDto>()
|
||||
?? throw new InvalidOperationException("Failed to deserialize created robot");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error creating robot");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing robot
|
||||
/// </summary>
|
||||
public async Task<RobotDto> UpdateAsync(Guid id, UpdateRobotRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"/api/robots/{id}", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotDto>()
|
||||
?? throw new InvalidOperationException("Failed to deserialize updated robot");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error updating robot {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a robot
|
||||
/// </summary>
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/robots/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error deleting robot {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
using System.Net.Http.Json;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Responses;
|
||||
|
||||
namespace RobotNet10.FleetManager.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// API service for robot model operations.
|
||||
/// Provides methods to interact with the robot model API endpoints.
|
||||
/// </summary>
|
||||
public class RobotModelApiService
|
||||
{
|
||||
private readonly HttpClient _httpClient;
|
||||
private readonly ILogger<RobotModelApiService>? _logger;
|
||||
|
||||
public RobotModelApiService(HttpClient httpClient, ILogger<RobotModelApiService>? logger = null)
|
||||
{
|
||||
_httpClient = httpClient;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get all robot models
|
||||
/// </summary>
|
||||
public async Task<List<RobotModelDto>> GetAllAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync("/api/robot-models");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<RobotModelDto>>() ?? new List<RobotModelDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting all robot models");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot model by ID
|
||||
/// </summary>
|
||||
public async Task<RobotModelDto?> GetByIdAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robot-models/{id}");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotModelDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search robot models
|
||||
/// </summary>
|
||||
public async Task<List<RobotModelDto>> SearchAsync(string query)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robot-models/search?query={Uri.EscapeDataString(query)}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<List<RobotModelDto>>() ?? new List<RobotModelDto>();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error searching robot models with query {Query}", query);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get usage information for a robot model
|
||||
/// </summary>
|
||||
public async Task<RobotModelUsageInfoDto> GetUsageInfoAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robot-models/{id}/usage");
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotModelUsageInfoDto>()
|
||||
?? throw new InvalidOperationException("Failed to deserialize usage info");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting usage info for robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a new robot model
|
||||
/// </summary>
|
||||
public async Task<RobotModelDto> CreateAsync(CreateRobotModelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PostAsJsonAsync("/api/robot-models", request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger?.LogError("Error creating robot model. Status: {StatusCode}, Response: {Error}",
|
||||
response.StatusCode, errorContent);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new InvalidOperationException($"Validation error: {errorContent}");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
return await response.Content.ReadFromJsonAsync<RobotModelDto>()
|
||||
?? throw new InvalidOperationException("Failed to deserialize created robot model");
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger?.LogError(ex, "HTTP error creating robot model");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error creating robot model");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update an existing robot model
|
||||
/// </summary>
|
||||
public async Task<RobotModelDto> UpdateAsync(Guid id, UpdateRobotModelRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.PutAsJsonAsync($"/api/robot-models/{id}", request);
|
||||
response.EnsureSuccessStatusCode();
|
||||
return await response.Content.ReadFromJsonAsync<RobotModelDto>()
|
||||
?? throw new InvalidOperationException("Failed to deserialize updated robot model");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error updating robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete a robot model
|
||||
/// </summary>
|
||||
public async Task DeleteAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/robot-models/{id}");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error deleting robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get robot model image
|
||||
/// </summary>
|
||||
public async Task<string?> GetImageAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.GetAsync($"/api/robot-models/{id}/image");
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.NotFound)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var imageBytes = await response.Content.ReadAsByteArrayAsync();
|
||||
return Convert.ToBase64String(imageBytes);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error getting image for robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Upload robot model image
|
||||
/// </summary>
|
||||
public async Task UploadImageAsync(Guid id, Stream imageStream, string fileName)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var content = new MultipartFormDataContent();
|
||||
using var streamContent = new StreamContent(imageStream);
|
||||
streamContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("image/png");
|
||||
content.Add(streamContent, "file", fileName);
|
||||
|
||||
var response = await _httpClient.PostAsync($"/api/robot-models/{id}/image", content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger?.LogError("Error uploading image. Status: {StatusCode}, Response: {Error}",
|
||||
response.StatusCode, errorContent);
|
||||
|
||||
if (response.StatusCode == System.Net.HttpStatusCode.BadRequest)
|
||||
{
|
||||
throw new InvalidOperationException($"Image upload validation error: {errorContent}");
|
||||
}
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger?.LogError(ex, "HTTP error uploading image for robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error uploading image for robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Delete robot model image
|
||||
/// </summary>
|
||||
public async Task DeleteImageAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var response = await _httpClient.DeleteAsync($"/api/robot-models/{id}/image");
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error deleting image for robot model {Id}", id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,874 @@
|
||||
using RobotNet10.FleetManager.Client;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.RobotModel;
|
||||
using RobotNet10.MapEditor.Services.API;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Edge;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Layout;
|
||||
using RobotNet10.MapEditor.Shared.DTOs.Node;
|
||||
|
||||
namespace RobotNet10.FleetManager.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Viewport state for SVG canvas
|
||||
/// </summary>
|
||||
public class ViewportState
|
||||
{
|
||||
public double ViewBoxX { get; set; }
|
||||
public double ViewBoxY { get; set; }
|
||||
public double ViewBoxWidth { get; set; }
|
||||
public double ViewBoxHeight { get; set; }
|
||||
public double ZoomLevel { get; set; } = 1.0;
|
||||
|
||||
public string ToViewBoxString() =>
|
||||
$"{ViewBoxX:F2} {ViewBoxY:F2} {ViewBoxWidth:F2} {ViewBoxHeight:F2}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Robot data for monitoring
|
||||
/// </summary>
|
||||
public class RobotMonitorData
|
||||
{
|
||||
public string RobotId { get; set; } = string.Empty;
|
||||
public Guid? ModelId { get; set; }
|
||||
public RobotModelDto? Model { get; set; } // Robot model info (Length, Width, etc.)
|
||||
public string? ModelImageBase64 { get; set; }
|
||||
public DateTime LastUpdateTime { get; set; }
|
||||
public RobotMonitorBoardcastData? Data { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// State management for RobotMonitor page
|
||||
/// </summary>
|
||||
public class RobotMonitorState(
|
||||
MapManagerApiService mapApiService,
|
||||
RobotApiService robotApiService,
|
||||
RobotModelApiService robotModelApiService,
|
||||
RobotStateHubClient? hubClient = null)
|
||||
{
|
||||
// ===== DATA =====
|
||||
public List<LayoutDto> Layouts { get; private set; } = [];
|
||||
public List<LayoutVersionDto> AvailableVersions { get; private set; } = [];
|
||||
public List<LayoutLevelDto> AvailableLevels { get; private set; } = [];
|
||||
public List<RobotDto> AvailableRobots { get; private set; } = [];
|
||||
|
||||
public Guid? SelectedLayoutId { get; set; }
|
||||
public Guid? SelectedVersionId { get; set; }
|
||||
public Guid? SelectedLevelId { get; set; }
|
||||
public LayoutLevelDto? Level { get; private set; }
|
||||
public List<NodeDto> Nodes { get; private set; } = [];
|
||||
public List<EdgeDto> Edges { get; private set; } = [];
|
||||
public byte[]? BackgroundImage { get; private set; }
|
||||
|
||||
// ===== ROBOTS =====
|
||||
public Dictionary<string, RobotMonitorData> Robots { get; private set; } = [];
|
||||
public string? SelectedRobotId { get; set; }
|
||||
|
||||
// ===== DISPLAY OPTIONS =====
|
||||
public bool ShowGrid { get; set; } = false;
|
||||
public bool ShowBackgroundImage { get; set; } = true;
|
||||
public bool ShowPath { get; set; } = false;
|
||||
public bool ShowName { get; set; } = true;
|
||||
public bool FollowRobot { get; set; } = false;
|
||||
public bool RobotInfoPanelExpanded { get; set; } = false;
|
||||
|
||||
// ===== VIEWPORT =====
|
||||
public ViewportState Viewport { get; } = new();
|
||||
|
||||
// ===== UI STATE =====
|
||||
public bool IsLoading { get; private set; }
|
||||
public string? ErrorMessage { get; set; }
|
||||
public bool IsMonitorDeactivated { get; private set; } = false;
|
||||
|
||||
// ===== EVENTS =====
|
||||
public event Action? OnDataChanged;
|
||||
public event Action? OnStateChanged;
|
||||
|
||||
// ===== DEPENDENCIES =====
|
||||
private readonly MapManagerApiService _mapApiService = mapApiService;
|
||||
private readonly RobotApiService _robotApiService = robotApiService;
|
||||
private readonly RobotModelApiService _robotModelApiService = robotModelApiService;
|
||||
private readonly RobotStateHubClient? _hubClient = hubClient;
|
||||
|
||||
// ===== ROBOT MODEL CACHE =====
|
||||
private readonly Dictionary<Guid, string> _robotModelImageCache = [];
|
||||
private readonly Dictionary<Guid, RobotModelDto> _robotModelCache = []; // modelId -> RobotModelDto
|
||||
private readonly Dictionary<string, Guid> _robotModelIdCache = []; // robotId -> modelId
|
||||
|
||||
// ===== TIMEOUT CONFIGURATION =====
|
||||
private const int RobotTimeoutSeconds = 10; // Remove robots after 10 seconds of no updates
|
||||
|
||||
/// <summary>
|
||||
/// Initialize monitor state
|
||||
/// </summary>
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// Load layouts
|
||||
await LoadLayoutsAsync();
|
||||
|
||||
// Load robots and their models
|
||||
await LoadRobotsAsync();
|
||||
|
||||
// Connect SignalR if available
|
||||
if (_hubClient != null)
|
||||
{
|
||||
await InitializeSignalRAsync();
|
||||
|
||||
// Register OnMonitorDeactivated event handler
|
||||
_hubClient.OnMonitorDeactivated += HandleMonitorDeactivated;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorMessage = $"Failed to initialize: {ex.Message}";
|
||||
// Log error for debugging
|
||||
System.Diagnostics.Debug.WriteLine($"RobotMonitor initialization error: {ex}");
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load all layouts
|
||||
/// </summary>
|
||||
public async Task LoadLayoutsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var allLayouts = await _mapApiService.SearchLayoutsAsync();
|
||||
// Only get active layouts
|
||||
Layouts = [.. allLayouts.Where(l => l.IsActive)];
|
||||
ErrorMessage = null; // Clear any previous errors
|
||||
|
||||
// Auto-select first layout, last version, last level
|
||||
if (Layouts.Count > 0)
|
||||
{
|
||||
var firstLayout = Layouts[0];
|
||||
SelectedLayoutId = firstLayout.Id;
|
||||
|
||||
// Load versions for first layout
|
||||
AvailableVersions = await _mapApiService.GetVersionsAsync(firstLayout.Id);
|
||||
|
||||
if (AvailableVersions.Count > 0)
|
||||
{
|
||||
var lastVersion = AvailableVersions.Last();
|
||||
SelectedVersionId = lastVersion.Id;
|
||||
|
||||
// Load levels for last version
|
||||
AvailableLevels = await _mapApiService.GetLevelsAsync(lastVersion.Id);
|
||||
|
||||
if (AvailableLevels.Count > 0)
|
||||
{
|
||||
var lastLevel = AvailableLevels.Last();
|
||||
SelectedLevelId = lastLevel.Id;
|
||||
|
||||
// Load layout data for last level
|
||||
await LoadLayoutDataAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Build detailed error message
|
||||
var errorDetails = new System.Text.StringBuilder();
|
||||
errorDetails.AppendLine($"Failed to load layouts: {ex.Message}");
|
||||
|
||||
// Add inner exception details if available
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorDetails.AppendLine($"Inner exception: {ex.InnerException.Message}");
|
||||
}
|
||||
|
||||
// Add stack trace for debugging (first few lines only)
|
||||
if (ex.StackTrace != null)
|
||||
{
|
||||
var stackLines = ex.StackTrace.Split('\n').Take(3);
|
||||
errorDetails.AppendLine($"Stack trace: {string.Join(" ", stackLines)}");
|
||||
}
|
||||
|
||||
ErrorMessage = errorDetails.ToString().Trim();
|
||||
Layouts = [];
|
||||
}
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle layout selection change
|
||||
/// </summary>
|
||||
public async Task OnLayoutSelectedAsync(Guid? layoutId)
|
||||
{
|
||||
SelectedLayoutId = layoutId;
|
||||
SelectedVersionId = null;
|
||||
SelectedLevelId = null;
|
||||
AvailableVersions = [];
|
||||
AvailableLevels = [];
|
||||
|
||||
if (layoutId.HasValue)
|
||||
{
|
||||
// Load versions for selected layout
|
||||
try
|
||||
{
|
||||
AvailableVersions = await _mapApiService.GetVersionsAsync(layoutId.Value);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorDetails = GetDetailedErrorMessage(ex, "load versions");
|
||||
ErrorMessage = errorDetails;
|
||||
AvailableVersions = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Clear layout data
|
||||
await LoadLayoutDataAsync();
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle version selection change
|
||||
/// </summary>
|
||||
public async Task OnVersionSelectedAsync(Guid? versionId)
|
||||
{
|
||||
SelectedVersionId = versionId;
|
||||
SelectedLevelId = null;
|
||||
AvailableLevels = [];
|
||||
|
||||
if (versionId.HasValue)
|
||||
{
|
||||
// Load levels for selected version
|
||||
try
|
||||
{
|
||||
AvailableLevels = await _mapApiService.GetLevelsAsync(versionId.Value);
|
||||
AvailableLevels = [.. AvailableLevels.OrderBy(l => l.LevelOrder)];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorDetails = GetDetailedErrorMessage(ex, "load levels");
|
||||
ErrorMessage = errorDetails;
|
||||
AvailableLevels = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Clear layout data
|
||||
await LoadLayoutDataAsync();
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle level selection change
|
||||
/// </summary>
|
||||
public async Task OnLevelSelectedAsync(Guid? levelId)
|
||||
{
|
||||
// Unsubscribe from old level if exists
|
||||
if (SelectedLevelId.HasValue && _hubClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hubClient.UnsubscribeFromLevelForMonitorAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore unsubscribe errors
|
||||
}
|
||||
}
|
||||
|
||||
SelectedLevelId = levelId;
|
||||
await LoadLayoutDataAsync();
|
||||
|
||||
// Subscribe to new level
|
||||
if (SelectedLevelId.HasValue && _hubClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hubClient.SubscribeToLevelForMonitorAsync(SelectedLevelId.Value);
|
||||
IsMonitorDeactivated = false; // Reset deactivated state when subscribing
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore subscription errors
|
||||
}
|
||||
}
|
||||
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle robot selection change (from dropdown)
|
||||
/// </summary>
|
||||
public void OnRobotSelected(string? robotId)
|
||||
{
|
||||
SelectRobot(robotId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load layout data for selected level
|
||||
/// </summary>
|
||||
public async Task LoadLayoutDataAsync()
|
||||
{
|
||||
if (!SelectedLevelId.HasValue)
|
||||
{
|
||||
Level = null;
|
||||
Nodes = [];
|
||||
Edges = [];
|
||||
BackgroundImage = null;
|
||||
NotifyStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoading = true;
|
||||
ErrorMessage = null;
|
||||
NotifyStateChanged();
|
||||
|
||||
try
|
||||
{
|
||||
// Load level info
|
||||
Level = await _mapApiService.GetLevelAsync(SelectedLevelId.Value);
|
||||
|
||||
// Load layout data (nodes, edges)
|
||||
var layoutData = await _mapApiService.GetLayoutDataAsync(SelectedLevelId.Value);
|
||||
Nodes = layoutData.Nodes?.ToList() ?? [];
|
||||
Edges = layoutData.Edges?.ToList() ?? [];
|
||||
|
||||
// Load background image
|
||||
try
|
||||
{
|
||||
BackgroundImage = await _mapApiService.GetLayoutImageAsync(SelectedLevelId.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
BackgroundImage = null;
|
||||
}
|
||||
|
||||
// Initialize viewport
|
||||
InitializeViewport();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorDetails = GetDetailedErrorMessage(ex, "load layout data");
|
||||
ErrorMessage = errorDetails;
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load robots from API
|
||||
/// </summary>
|
||||
private async Task LoadRobotsAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
AvailableRobots = await _robotApiService.GetAllAsync();
|
||||
|
||||
// Cache robot model IDs
|
||||
foreach (var robot in AvailableRobots)
|
||||
{
|
||||
if (robot.ModelId != Guid.Empty)
|
||||
{
|
||||
_robotModelIdCache[robot.RobotId] = robot.ModelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorDetails = GetDetailedErrorMessage(ex, "load robots");
|
||||
ErrorMessage = errorDetails;
|
||||
AvailableRobots = [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize SignalR connection
|
||||
/// </summary>
|
||||
private async Task InitializeSignalRAsync()
|
||||
{
|
||||
if (_hubClient == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Register event handlers
|
||||
_hubClient.OnMonitorBoardcastUpdate += HandleStateUpdate;
|
||||
//_hubClient.OnVisualizationUpdate += HandleVisualizationUpdate;
|
||||
_hubClient.OnConnectionError += HandleConnectionError;
|
||||
// Connect
|
||||
await _hubClient.ConnectAsync();
|
||||
|
||||
// Subscribe to all robots
|
||||
// Subscribe to level will be done in OnLevelSelectedAsync when level is selected
|
||||
if (SelectedLevelId.HasValue && _hubClient != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hubClient.SubscribeToLevelForMonitorAsync(SelectedLevelId.Value);
|
||||
IsMonitorDeactivated = false; // Reset deactivated state when subscribing
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore subscription errors
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var errorDetails = GetDetailedErrorMessage(ex, "connect SignalR");
|
||||
ErrorMessage = errorDetails;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle monitor deactivated event (when connection is evicted due to max connections)
|
||||
/// </summary>
|
||||
private void HandleMonitorDeactivated()
|
||||
{
|
||||
IsMonitorDeactivated = true;
|
||||
ErrorMessage = "Monitor connection deactivated: Maximum 5 connections per level reached. Another connection has taken your place.";
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handle SignalR connection error
|
||||
/// </summary>
|
||||
private void HandleConnectionError(string error)
|
||||
{
|
||||
ErrorMessage = $"SignalR connection error: {error}";
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize viewport to fit the image bounds
|
||||
/// </summary>
|
||||
private void InitializeViewport()
|
||||
{
|
||||
if (Level?.EditorSettings != null)
|
||||
{
|
||||
var settings = Level.EditorSettings;
|
||||
|
||||
// Calculate physical dimensions
|
||||
double physicalWidth = (settings.ImageWidth ?? 1000) * settings.Resolution;
|
||||
double physicalHeight = (settings.ImageHeight ?? 500) * settings.Resolution;
|
||||
|
||||
// Set viewport to fit image
|
||||
Viewport.ViewBoxX = 0;
|
||||
Viewport.ViewBoxY = 0;
|
||||
Viewport.ViewBoxWidth = physicalWidth;
|
||||
Viewport.ViewBoxHeight = physicalHeight;
|
||||
Viewport.ZoomLevel = 1.0;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Default viewport
|
||||
Viewport.ViewBoxX = 0;
|
||||
Viewport.ViewBoxY = 0;
|
||||
Viewport.ViewBoxWidth = 50;
|
||||
Viewport.ViewBoxHeight = 30;
|
||||
Viewport.ZoomLevel = 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// SIGNALR HANDLERS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Handle StateMsg update from SignalR
|
||||
/// </summary>
|
||||
private void HandleStateUpdate(RobotMonitorBoardcastData state)
|
||||
{
|
||||
var robotId = state.RobotId;
|
||||
|
||||
// Get or create robot data
|
||||
if (!Robots.TryGetValue(robotId, out var robotData))
|
||||
{
|
||||
robotData = new RobotMonitorData
|
||||
{
|
||||
RobotId = robotId,
|
||||
ModelId = _robotModelIdCache.TryGetValue(robotId, out var modelId) ? modelId : null
|
||||
};
|
||||
Robots[robotId] = robotData;
|
||||
|
||||
// Load robot model image asynchronously
|
||||
_ = LoadRobotModelImageAsync(robotData);
|
||||
}
|
||||
|
||||
// Update position and state
|
||||
robotData.Data = state;
|
||||
robotData.LastUpdateTime = DateTime.UtcNow;
|
||||
|
||||
// If FollowRobot and this is selected robot, update viewport
|
||||
if (FollowRobot && SelectedRobotId == robotId)
|
||||
{
|
||||
FocusOnRobot(robotId);
|
||||
}
|
||||
|
||||
// Remove inactive robots periodically (check every update)
|
||||
RemoveInactiveRobots();
|
||||
|
||||
OnDataChanged?.Invoke();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load robot model image and info
|
||||
/// </summary>
|
||||
private async Task LoadRobotModelImageAsync(RobotMonitorData robotData)
|
||||
{
|
||||
if (!robotData.ModelId.HasValue) return;
|
||||
|
||||
try
|
||||
{
|
||||
// Load model info if not cached
|
||||
if (!_robotModelCache.TryGetValue(robotData.ModelId.Value, out var model))
|
||||
{
|
||||
model = await _robotModelApiService.GetByIdAsync(robotData.ModelId.Value);
|
||||
if (model != null)
|
||||
{
|
||||
_robotModelCache[robotData.ModelId.Value] = model;
|
||||
}
|
||||
}
|
||||
|
||||
if (model != null)
|
||||
{
|
||||
robotData.Model = model;
|
||||
}
|
||||
|
||||
// Check image cache first
|
||||
if (_robotModelImageCache.TryGetValue(robotData.ModelId.Value, out var cachedImage))
|
||||
{
|
||||
robotData.ModelImageBase64 = cachedImage;
|
||||
NotifyStateChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load image from API
|
||||
var imageBase64 = await _robotModelApiService.GetImageAsync(robotData.ModelId.Value);
|
||||
if (imageBase64 != null)
|
||||
{
|
||||
_robotModelImageCache[robotData.ModelId.Value] = imageBase64;
|
||||
robotData.ModelImageBase64 = imageBase64;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore errors loading images
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// ROBOT SELECTION
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Select a robot
|
||||
/// </summary>
|
||||
public void SelectRobot(string? robotId)
|
||||
{
|
||||
SelectedRobotId = robotId;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// VIEWPORT OPERATIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Pan the viewport by delta (in SVG units)
|
||||
/// </summary>
|
||||
public void Pan(double deltaX, double deltaY)
|
||||
{
|
||||
Viewport.ViewBoxX += deltaX;
|
||||
Viewport.ViewBoxY += deltaY;
|
||||
NotifyStateChanged(); // Use immediate for smooth panning
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zoom the viewport around a point (in SVG/world coordinates)
|
||||
/// </summary>
|
||||
public void Zoom(double factor, double svgCenterX, double svgCenterY)
|
||||
{
|
||||
// Limit zoom level
|
||||
var newZoom = Viewport.ZoomLevel * factor;
|
||||
if (newZoom < 0.1 || newZoom > 10) return;
|
||||
|
||||
// Calculate the ratio of the cursor position within the current viewBox
|
||||
var ratioX = (svgCenterX - Viewport.ViewBoxX) / Viewport.ViewBoxWidth;
|
||||
var ratioY = (svgCenterY - Viewport.ViewBoxY) / Viewport.ViewBoxHeight;
|
||||
|
||||
// New dimensions after zoom
|
||||
var newWidth = Viewport.ViewBoxWidth / factor;
|
||||
var newHeight = Viewport.ViewBoxHeight / factor;
|
||||
|
||||
// Adjust ViewBox position so the cursor point stays at the same world position
|
||||
Viewport.ViewBoxX = svgCenterX - ratioX * newWidth;
|
||||
Viewport.ViewBoxY = svgCenterY - ratioY * newHeight;
|
||||
Viewport.ViewBoxWidth = newWidth;
|
||||
Viewport.ViewBoxHeight = newHeight;
|
||||
Viewport.ZoomLevel = newZoom;
|
||||
|
||||
NotifyStateChanged(); // Use immediate for smooth zooming
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Zoom in/out centered at the viewport center
|
||||
/// </summary>
|
||||
public void ZoomAtCenter(double factor)
|
||||
{
|
||||
// Limit zoom level
|
||||
var newZoom = Viewport.ZoomLevel * factor;
|
||||
if (newZoom < 0.1 || newZoom > 10) return;
|
||||
|
||||
// Calculate center of current viewport
|
||||
var centerX = Viewport.ViewBoxX + Viewport.ViewBoxWidth / 2;
|
||||
var centerY = Viewport.ViewBoxY + Viewport.ViewBoxHeight / 2;
|
||||
|
||||
// New dimensions after zoom
|
||||
var newWidth = Viewport.ViewBoxWidth / factor;
|
||||
var newHeight = Viewport.ViewBoxHeight / factor;
|
||||
|
||||
// Adjust viewBox to keep the same center point
|
||||
Viewport.ViewBoxX = centerX - newWidth / 2;
|
||||
Viewport.ViewBoxY = centerY - newHeight / 2;
|
||||
Viewport.ViewBoxWidth = newWidth;
|
||||
Viewport.ViewBoxHeight = newHeight;
|
||||
Viewport.ZoomLevel = newZoom;
|
||||
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fit viewport to image bounds
|
||||
/// </summary>
|
||||
public void FitToScreen()
|
||||
{
|
||||
InitializeViewport();
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Focus viewport on selected robot (with optional zoom)
|
||||
/// </summary>
|
||||
public void FocusOnRobot(string robotId, bool zoomToFit = false)
|
||||
{
|
||||
if (!Robots.TryGetValue(robotId, out var robot) || robot is null || robot.Data is null)
|
||||
return;
|
||||
|
||||
// Center viewport on robot
|
||||
var (X, Y) = WorldToSvg(robot.Data.AgvPosition.X, robot.Data.AgvPosition.Y);
|
||||
|
||||
if (zoomToFit && robot.Model != null)
|
||||
{
|
||||
// Zoom to fit robot with some padding
|
||||
var padding = 2.0; // meters padding around robot
|
||||
var robotSize = Math.Max(robot.Model.Length, robot.Model.Width);
|
||||
var viewSize = robotSize + padding * 2;
|
||||
|
||||
Viewport.ViewBoxWidth = viewSize;
|
||||
Viewport.ViewBoxHeight = viewSize;
|
||||
Viewport.ZoomLevel = GetPhysicalDimensions().Width / viewSize;
|
||||
}
|
||||
|
||||
Viewport.ViewBoxX = X - Viewport.ViewBoxWidth / 2;
|
||||
Viewport.ViewBoxY = Y - Viewport.ViewBoxHeight / 2;
|
||||
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// COORDINATE TRANSFORM
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Get physical dimensions of the layout
|
||||
/// </summary>
|
||||
public (double Width, double Height) GetPhysicalDimensions()
|
||||
{
|
||||
if (Level?.EditorSettings == null)
|
||||
return (50, 30);
|
||||
|
||||
var settings = Level.EditorSettings;
|
||||
return (
|
||||
(settings.ImageWidth ?? 1000) * settings.Resolution,
|
||||
(settings.ImageHeight ?? 500) * settings.Resolution
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform world coordinates (layout) to SVG coordinates
|
||||
/// </summary>
|
||||
public (double X, double Y) WorldToSvg(double worldX, double worldY)
|
||||
{
|
||||
var (_, physicalHeight) = GetPhysicalDimensions();
|
||||
|
||||
var originX = Level != null && Level.EditorSettings != null ? Level.EditorSettings.OriginX : 0;
|
||||
var originY = Level != null && Level.EditorSettings != null ? Level.EditorSettings.OriginY : 0;
|
||||
return (worldX - originX, physicalHeight - (worldY - originY));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform SVG coordinates to world coordinates (layout)
|
||||
/// </summary>
|
||||
public (double X, double Y) SvgToWorld(double svgX, double svgY)
|
||||
{
|
||||
var (_, physicalHeight) = GetPhysicalDimensions();
|
||||
|
||||
var originX = Level != null && Level.EditorSettings != null ? Level.EditorSettings.OriginX : 0;
|
||||
var originY = Level != null && Level.EditorSettings != null ? Level.EditorSettings.OriginY : 0;
|
||||
return (svgX + originX, physicalHeight - svgY + originY);
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// DISPLAY OPTIONS
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Toggle follow robot mode
|
||||
/// </summary>
|
||||
public void ToggleFollowRobot()
|
||||
{
|
||||
FollowRobot = !FollowRobot;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle robot info panel visibility
|
||||
/// </summary>
|
||||
public void ToggleRobotInfoPanel()
|
||||
{
|
||||
RobotInfoPanelExpanded = !RobotInfoPanelExpanded;
|
||||
NotifyStateChanged();
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// UTILITIES
|
||||
// ==========================================
|
||||
|
||||
/// <summary>
|
||||
/// Remove robots that haven't been updated for a while
|
||||
/// </summary>
|
||||
private void RemoveInactiveRobots()
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var timeout = TimeSpan.FromSeconds(RobotTimeoutSeconds);
|
||||
var robotsToRemove = new List<string>();
|
||||
|
||||
foreach (var kvp in Robots)
|
||||
{
|
||||
if (now - kvp.Value.LastUpdateTime > timeout)
|
||||
{
|
||||
robotsToRemove.Add(kvp.Key);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var robotId in robotsToRemove)
|
||||
{
|
||||
Robots.Remove(robotId);
|
||||
|
||||
// Clear selection if removed robot was selected
|
||||
if (SelectedRobotId == robotId)
|
||||
{
|
||||
SelectedRobotId = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cleanup resources (unsubscribe from SignalR, etc.)
|
||||
/// </summary>
|
||||
public async Task CleanupAsync()
|
||||
{
|
||||
if (_hubClient != null)
|
||||
{
|
||||
// Unregister event handlers
|
||||
_hubClient.OnMonitorBoardcastUpdate -= HandleStateUpdate;
|
||||
//_hubClient.OnVisualizationUpdate -= HandleVisualizationUpdate;
|
||||
_hubClient.OnConnectionError -= HandleConnectionError;
|
||||
_hubClient.OnMonitorDeactivated -= HandleMonitorDeactivated;
|
||||
|
||||
// Unsubscribe from level before disconnecting
|
||||
try
|
||||
{
|
||||
await _hubClient.UnsubscribeFromLevelForMonitorAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore unsubscribe errors
|
||||
}
|
||||
|
||||
// Disconnect
|
||||
try
|
||||
{
|
||||
await _hubClient.DisconnectAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disconnect errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get detailed error message from exception
|
||||
/// </summary>
|
||||
private static string GetDetailedErrorMessage(Exception ex, string operation)
|
||||
{
|
||||
var errorDetails = new System.Text.StringBuilder();
|
||||
errorDetails.AppendLine($"Failed to {operation}: {ex.Message}");
|
||||
|
||||
// Add inner exception details if available
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
errorDetails.AppendLine($"Inner exception: {ex.InnerException.Message}");
|
||||
}
|
||||
|
||||
// For HttpRequestException, try to extract more details
|
||||
if (ex is System.Net.Http.HttpRequestException httpEx)
|
||||
{
|
||||
var message = httpEx.Message;
|
||||
|
||||
// Extract status code if present
|
||||
if (message.Contains("404"))
|
||||
{
|
||||
errorDetails.AppendLine("Status: 404 Not Found - The requested resource was not found on the server.");
|
||||
}
|
||||
else if (message.Contains("401"))
|
||||
{
|
||||
errorDetails.AppendLine("Status: 401 Unauthorized - Authentication required.");
|
||||
}
|
||||
else if (message.Contains("403"))
|
||||
{
|
||||
errorDetails.AppendLine("Status: 403 Forbidden - Access denied.");
|
||||
}
|
||||
else if (message.Contains("500"))
|
||||
{
|
||||
errorDetails.AppendLine("Status: 500 Internal Server Error - Server encountered an error.");
|
||||
}
|
||||
|
||||
// Add URL if present in message
|
||||
if (message.Contains("URL:"))
|
||||
{
|
||||
var urlStart = message.IndexOf("URL:");
|
||||
if (urlStart >= 0)
|
||||
{
|
||||
var urlPart = message[urlStart..];
|
||||
errorDetails.AppendLine(urlPart);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errorDetails.ToString().Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Notify state changed immediately (for critical updates)
|
||||
/// </summary>
|
||||
public void NotifyStateChanged()
|
||||
{
|
||||
OnStateChanged?.Invoke();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using RobotNet.VDA5050.State;
|
||||
using RobotNet10.FleetManager.Shared.DTOs.Robot;
|
||||
|
||||
namespace RobotNet10.FleetManager.Client.Services;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR Hub Client for robot state and visualization updates.
|
||||
/// Manages connection to the SignalR hub and provides methods to subscribe/unsubscribe to robot updates.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This client automatically handles reconnection and provides events for state and visualization updates.
|
||||
/// Dispose the client when done to properly clean up the connection.
|
||||
/// </remarks>
|
||||
public class RobotStateHubClient : IAsyncDisposable
|
||||
{
|
||||
private readonly HubConnection _hubConnection;
|
||||
private readonly ILogger<RobotStateHubClient>? _logger;
|
||||
|
||||
public event Action<RobotMonitorBoardcastData>? OnMonitorBoardcastUpdate;
|
||||
public event Action<StateMsg>? OnStateUpdate;
|
||||
public event Action<string>? OnConnectionError;
|
||||
public event Action? OnMonitorDeactivated;
|
||||
|
||||
public RobotStateHubClient(NavigationManager navigationManager, ILogger<RobotStateHubClient>? logger = null)
|
||||
{
|
||||
_logger = logger;
|
||||
var hubUrl = navigationManager.ToAbsoluteUri("/hubs/robot-state");
|
||||
|
||||
_hubConnection = new HubConnectionBuilder()
|
||||
.WithUrl(hubUrl)
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
// Register event handlers
|
||||
_hubConnection.On<StateMsg>("OnStateUpdate", (state) =>
|
||||
{
|
||||
OnStateUpdate?.Invoke(state);
|
||||
});
|
||||
|
||||
_hubConnection.On<RobotMonitorBoardcastData>("OnMonitorUpdate", (state) =>
|
||||
{
|
||||
OnMonitorBoardcastUpdate?.Invoke(state);
|
||||
});
|
||||
|
||||
_hubConnection.On("OnMonitorDeactivated", () =>
|
||||
{
|
||||
OnMonitorDeactivated?.Invoke();
|
||||
});
|
||||
|
||||
_hubConnection.Closed += async (error) =>
|
||||
{
|
||||
if (error != null)
|
||||
{
|
||||
_logger?.LogError(error, "SignalR connection closed with error");
|
||||
OnConnectionError?.Invoke(error.Message);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogInformation("SignalR connection closed");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Reconnecting += async (error) =>
|
||||
{
|
||||
_logger?.LogWarning(error, "SignalR connection reconnecting");
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
|
||||
_hubConnection.Reconnected += async (connectionId) =>
|
||||
{
|
||||
_logger?.LogInformation("SignalR connection reconnected with ID {ConnectionId}", connectionId);
|
||||
await Task.CompletedTask;
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Connect to the SignalR hub
|
||||
/// </summary>
|
||||
public async Task ConnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
await _hubConnection.StartAsync();
|
||||
_logger?.LogInformation("SignalR hub connected");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error connecting to SignalR hub");
|
||||
OnConnectionError?.Invoke(ex.Message);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disconnect from the SignalR hub
|
||||
/// </summary>
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State != HubConnectionState.Disconnected)
|
||||
{
|
||||
await _hubConnection.StopAsync();
|
||||
_logger?.LogInformation("SignalR hub disconnected");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error disconnecting from SignalR hub");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to receive updates for a specific robot
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
public async Task SubscribeToRobotAsync(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.InvokeAsync("SubscribeToRobot", robotId);
|
||||
_logger?.LogInformation("Subscribed to robot {RobotId}", robotId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("Cannot subscribe to robot {RobotId}: Hub not connected", robotId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error subscribing to robot {RobotId}", robotId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from updates for a specific robot
|
||||
/// </summary>
|
||||
/// <param name="robotId">Robot identifier (serialNumber)</param>
|
||||
public async Task UnsubscribeFromRobotAsync(string robotId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.InvokeAsync("UnsubscribeFromRobot", robotId);
|
||||
_logger?.LogInformation("Unsubscribed from robot {RobotId}", robotId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error unsubscribing from robot {RobotId}", robotId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to receive monitor updates for a specific levelId
|
||||
/// Each connection can only subscribe to one levelId at a time.
|
||||
/// Maximum 5 connections per levelId (FIFO).
|
||||
/// </summary>
|
||||
/// <param name="levelId">Level identifier (LayoutLevel.Id)</param>
|
||||
public async Task SubscribeToLevelForMonitorAsync(Guid levelId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.InvokeAsync("SubscribeToLevelForMonitor", levelId);
|
||||
_logger?.LogInformation("Subscribed to level {LevelId} for monitor", levelId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger?.LogWarning("Cannot subscribe to level {LevelId}: Hub not connected", levelId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error subscribing to level {LevelId} for monitor", levelId);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unsubscribe from monitor updates for the current level
|
||||
/// </summary>
|
||||
public async Task UnsubscribeFromLevelForMonitorAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_hubConnection.State == HubConnectionState.Connected)
|
||||
{
|
||||
await _hubConnection.InvokeAsync("UnsubscribeFromLevelForMonitor");
|
||||
_logger?.LogInformation("Unsubscribed from level for monitor");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger?.LogError(ex, "Error unsubscribing from level for monitor");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the current connection state
|
||||
/// </summary>
|
||||
public HubConnectionState ConnectionState => _hubConnection.State;
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_hubConnection != null)
|
||||
{
|
||||
await DisconnectAsync();
|
||||
await _hubConnection.DisposeAsync();
|
||||
}
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using RobotNet10.ScriptEngine.Shared;
|
||||
using System.Collections.Immutable;
|
||||
using RobotNet10.FleetManager.Script.Shared;
|
||||
|
||||
|
||||
namespace RobotNet10.FleetManager.Client.Services;
|
||||
|
||||
public class ScriptEngineResource : IScriptEngineResource
|
||||
{
|
||||
public Type AppGlobalType => FleetManagerScriptEngineResource.GlobalType;
|
||||
|
||||
public ImmutableArray<string> UsingNamespaces => FleetManagerScriptEngineResource.UsingNamespaces;
|
||||
|
||||
public ImmutableArray<string> Modules => FleetManagerScriptEngineResource.Modules;
|
||||
|
||||
public ImmutableArray<string> DocModules => FleetManagerScriptEngineResource.DocModules;
|
||||
|
||||
public IDictionary<string, object?> GetMissionGlobals(Guid id, CancellationToken cancellationToken)
|
||||
=> new Dictionary<string, object?>();
|
||||
|
||||
public IDictionary<string, object?> GetTaskGlobals()
|
||||
=> new Dictionary<string, object?>();
|
||||
}
|
||||
Reference in New Issue
Block a user