137 lines
6.1 KiB
C#
137 lines
6.1 KiB
C#
using Microsoft.AspNetCore.SignalR;
|
|
|
|
namespace Extrudex.API.Hubs;
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
///
|
|
/// <para>Usage flow:</para>
|
|
/// <list type="number">
|
|
/// <item>Client connects to /hubs/printer</item>
|
|
/// <item>Client calls <see cref="JoinPrinterGroup"/> with a printer ID</item>
|
|
/// <item>Server adds the connection to a SignalR group named after the printer ID</item>
|
|
/// <item>When the backend detects a status change, it calls
|
|
/// <see cref="PrinterHubExtensions.PushPrinterStatusAsync"/>
|
|
/// which broadcasts to all subscribers of that printer</item>
|
|
/// </list>
|
|
///
|
|
/// <para>Group naming: <c>printer:{printerId}</c> (lowercase GUID).</para>
|
|
///
|
|
/// <para>Typed client: <see cref="IPrinterClient"/> — all server-to-client
|
|
/// calls go through this interface for compile-time safety.</para>
|
|
/// </summary>
|
|
public class PrinterHub : Hub<IPrinterClient>
|
|
{
|
|
/// <summary>
|
|
/// 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 <see cref="LeavePrinterGroup"/>.
|
|
/// </summary>
|
|
/// <param name="printerId">
|
|
/// The unique identifier of the printer to subscribe to.
|
|
/// The GUID is normalized to lowercase for consistent group naming.
|
|
/// </param>
|
|
/// <exception cref="HubException">
|
|
/// Thrown if <paramref name="printerId"/> cannot be parsed as a valid GUID.
|
|
/// </exception>
|
|
public async Task JoinPrinterGroup(Guid printerId)
|
|
{
|
|
var groupName = PrinterGroupName(printerId);
|
|
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the calling connection from the SignalR group for a specific printer.
|
|
/// After leaving, the client will no longer receive updates for that printer.
|
|
/// </summary>
|
|
/// <param name="printerId">
|
|
/// The unique identifier of the printer to unsubscribe from.
|
|
/// </param>
|
|
public async Task LeavePrinterGroup(Guid printerId)
|
|
{
|
|
var groupName = PrinterGroupName(printerId);
|
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overrides <see cref="Hub.OnDisconnectedAsync"/> to perform cleanup.
|
|
/// SignalR automatically removes disconnected connections from all groups,
|
|
/// so no manual cleanup is required here.
|
|
/// </summary>
|
|
/// <param name="exception">Exception that caused the disconnection, if any.</param>
|
|
public override Task OnDisconnectedAsync(Exception? exception)
|
|
{
|
|
// SignalR automatically removes the connection from all groups on disconnect.
|
|
// No manual cleanup needed.
|
|
return base.OnDisconnectedAsync(exception);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the SignalR group name for a given printer ID.
|
|
/// Format: <c>printer:{printerId}</c> (lowercase to avoid case-sensitivity issues).
|
|
/// </summary>
|
|
/// <param name="printerId">The unique identifier of the printer.</param>
|
|
/// <returns>A consistent, lowercase group name string.</returns>
|
|
internal static string PrinterGroupName(Guid printerId) =>
|
|
$"printer:{printerId.ToString().ToLowerInvariant()}";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Extension methods for pushing real-time printer updates through
|
|
/// the <see cref="IHubContext{T}"/> of <see cref="PrinterHub"/>.
|
|
///
|
|
/// 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.
|
|
/// </summary>
|
|
public static class PrinterHubExtensions
|
|
{
|
|
/// <summary>
|
|
/// 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).
|
|
/// </summary>
|
|
/// <param name="hubContext">The hub context injected via DI.</param>
|
|
/// <param name="printerId">The unique identifier of the printer that changed.</param>
|
|
/// <param name="status">The new status string (e.g., "Idle", "Printing", "Offline").</param>
|
|
/// <param name="lastSeenAt">Timestamp (UTC) of when the status was observed, or null if unknown.</param>
|
|
/// <returns>A Task that completes when the message has been sent to all group members.</returns>
|
|
public static async Task PushPrinterStatusAsync(
|
|
this IHubContext<PrinterHub, IPrinterClient> hubContext,
|
|
Guid printerId,
|
|
string status,
|
|
DateTime? lastSeenAt = null)
|
|
{
|
|
var groupName = PrinterHub.PrinterGroupName(printerId);
|
|
await hubContext.Clients.Group(groupName)
|
|
.PrinterStatusChanged(printerId, status, lastSeenAt);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="hubContext">The hub context injected via DI.</param>
|
|
/// <param name="printerId">The unique identifier of the printer.</param>
|
|
/// <param name="isActive">Whether the printer is currently active and accepting jobs.</param>
|
|
/// <param name="lastSeenAt">Timestamp (UTC) of the last telemetry from the printer, or null.</param>
|
|
/// <returns>A Task that completes when the message has been sent to all group members.</returns>
|
|
public static async Task PushPrinterHeartbeatAsync(
|
|
this IHubContext<PrinterHub, IPrinterClient> hubContext,
|
|
Guid printerId,
|
|
bool isActive,
|
|
DateTime? lastSeenAt = null)
|
|
{
|
|
var groupName = PrinterHub.PrinterGroupName(printerId);
|
|
await hubContext.Clients.Group(groupName)
|
|
.PrinterHeartbeat(printerId, isActive, lastSeenAt);
|
|
}
|
|
} |