using Microsoft.AspNetCore.SignalR; namespace Extrudex.API.Hubs; /// /// SignalR hub for real-time printer status updates. /// /// Clients connect to this hub to receive push notifications when /// a printer's status changes (e.g., Idle → Printing, Offline → Idle) /// or when a heartbeat confirms the printer is still reachable. /// /// Usage flow: /// /// Client connects to /hubs/printer /// Client calls with a printer ID /// Server adds the connection to a SignalR group named after the printer ID /// When the backend detects a status change, it calls /// /// which broadcasts to all subscribers of that printer /// /// /// Group naming: printer:{printerId} (lowercase GUID). /// /// Typed client: — all server-to-client /// calls go through this interface for compile-time safety. /// public class PrinterHub : Hub { /// /// Adds the calling connection to the SignalR group for a specific printer. /// Once joined, the client will receive all status updates and heartbeats /// for that printer until it disconnects or calls . /// /// /// The unique identifier of the printer to subscribe to. /// The GUID is normalized to lowercase for consistent group naming. /// /// /// Thrown if cannot be parsed as a valid GUID. /// public async Task JoinPrinterGroup(Guid printerId) { var groupName = PrinterGroupName(printerId); await Groups.AddToGroupAsync(Context.ConnectionId, groupName); } /// /// Removes the calling connection from the SignalR group for a specific printer. /// After leaving, the client will no longer receive updates for that printer. /// /// /// The unique identifier of the printer to unsubscribe from. /// public async Task LeavePrinterGroup(Guid printerId) { var groupName = PrinterGroupName(printerId); await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName); } /// /// Overrides to perform cleanup. /// SignalR automatically removes disconnected connections from all groups, /// so no manual cleanup is required here. /// /// Exception that caused the disconnection, if any. public override Task OnDisconnectedAsync(Exception? exception) { // SignalR automatically removes the connection from all groups on disconnect. // No manual cleanup needed. return base.OnDisconnectedAsync(exception); } /// /// Returns the SignalR group name for a given printer ID. /// Format: printer:{printerId} (lowercase to avoid case-sensitivity issues). /// /// The unique identifier of the printer. /// A consistent, lowercase group name string. internal static string PrinterGroupName(Guid printerId) => $"printer:{printerId.ToString().ToLowerInvariant()}"; } /// /// Extension methods for pushing real-time printer updates through /// the of . /// /// These methods are intended to be called from background services /// (e.g., MQTT message handlers, Moonraker pollers) or other /// server-side code that detects a printer state change. /// public static class PrinterHubExtensions { /// /// Pushes a printer status change to all clients subscribed to /// the given printer's SignalR group. /// /// Call this from any background service or controller when a printer's /// operational status changes (e.g., a Bambu Lab MQTT message reports /// the printer started printing, or a Moonraker poller detects an error). /// /// The hub context injected via DI. /// The unique identifier of the printer that changed. /// The new status string (e.g., "Idle", "Printing", "Offline"). /// Timestamp (UTC) of when the status was observed, or null if unknown. /// A Task that completes when the message has been sent to all group members. public static async Task PushPrinterStatusAsync( this IHubContext hubContext, Guid printerId, string status, DateTime? lastSeenAt = null) { var groupName = PrinterHub.PrinterGroupName(printerId); await hubContext.Clients.Group(groupName) .PrinterStatusChanged(printerId, status, lastSeenAt); } /// /// Pushes a heartbeat signal to all clients subscribed to the given /// printer's SignalR group. Use this for lightweight "still alive" /// notifications that don't require a full status payload. /// /// The hub context injected via DI. /// The unique identifier of the printer. /// Whether the printer is currently active and accepting jobs. /// Timestamp (UTC) of the last telemetry from the printer, or null. /// A Task that completes when the message has been sent to all group members. public static async Task PushPrinterHeartbeatAsync( this IHubContext hubContext, Guid printerId, bool isActive, DateTime? lastSeenAt = null) { var groupName = PrinterHub.PrinterGroupName(printerId); await hubContext.Clients.Group(groupName) .PrinterHeartbeat(printerId, isActive, lastSeenAt); } }