initial commit
This commit is contained in:
31
backend/API/Hubs/IPrinterClient.cs
Normal file
31
backend/API/Hubs/IPrinterClient.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
namespace Extrudex.API.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Strongly-typed client interface for the SignalR PrinterHub.
|
||||
/// Defines the methods that the server can invoke on connected clients
|
||||
/// to push real-time printer status updates.
|
||||
/// </summary>
|
||||
public interface IPrinterClient
|
||||
{
|
||||
/// <summary>
|
||||
/// Pushes a full printer status update to all clients subscribed
|
||||
/// to a specific printer's group. Fired whenever a printer's
|
||||
/// operational status changes (e.g., Idle → Printing, Offline → Idle).
|
||||
/// </summary>
|
||||
/// <param name="printerId">The unique identifier of the printer that changed.</param>
|
||||
/// <param name="status">The new status value (e.g., "Idle", "Printing", "Offline", "Error", "Paused").</param>
|
||||
/// <param name="lastSeenAt">Timestamp (UTC) of when this status was last observed.</param>
|
||||
/// <returns>A Task that completes when the client has processed the update.</returns>
|
||||
Task PrinterStatusChanged(Guid printerId, string status, DateTime? lastSeenAt);
|
||||
|
||||
/// <summary>
|
||||
/// Pushes a lightweight heartbeat to confirm that a printer is still
|
||||
/// reachable and its connection is alive. Useful for dashboards that
|
||||
/// display online/offline indicators without requiring a full status payload.
|
||||
/// </summary>
|
||||
/// <param name="printerId">The unique identifier of the printer.</param>
|
||||
/// <param name="isActive">Whether the printer is currently active and available for jobs.</param>
|
||||
/// <param name="lastSeenAt">Timestamp (UTC) of the last telemetry received from the printer.</param>
|
||||
/// <returns>A Task that completes when the client has processed the heartbeat.</returns>
|
||||
Task PrinterHeartbeat(Guid printerId, bool isActive, DateTime? lastSeenAt);
|
||||
}
|
||||
137
backend/API/Hubs/PrinterHub.cs
Normal file
137
backend/API/Hubs/PrinterHub.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user