Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 142 additions & 37 deletions MakerPrompt.Shared/Components/ControlPanel.razor
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
<div class="row g-3">

@* ── Left: Position + Webcam ── *@
@if (SupportsDirectControl)
{
<div class="col-lg-6 col-md-12">
<div class="card">
<div class="card-header d-flex align-items-center gap-2 py-2">
Expand Down Expand Up @@ -144,6 +146,15 @@
<WebcamPanel />
</div>
</div>
}
else
{
<div class="col-lg-6 col-md-12">
<div class="mt-3">
<WebcamPanel />
</div>
</div>
}

@* ── Right: Temps + Extrude + Speed/Flow + Calibration ── *@
<div class="col-lg-6 col-md-12">
Expand All @@ -155,18 +166,21 @@
<span class="fw-semibold">@Localizer[Resources.ControlPanel_Heating]</span>
</div>
<div class="card-body pb-1">
@* Preheat presets *@
<div class="d-flex flex-wrap gap-1 mb-3">
@foreach (var (label, hotend, bed) in PreheatProfiles)
{
var h = hotend; var b = bed;
<button type="button" class="btn btn-sm btn-outline-secondary"
@onclick="() => PreheatAsync(h, b)" disabled="@(!IsConnected)"
title="@($"Hotend {hotend}°C / Bed {bed}°C")">
@label
</button>
}
</div>
@* Preheat presets (control-capable backends only) *@
@if (SupportsDirectControl)
{
<div class="d-flex flex-wrap gap-1 mb-3">
@foreach (var (label, hotend, bed) in PreheatProfiles)
{
var h = hotend; var b = bed;
<button type="button" class="btn btn-sm btn-outline-secondary"
@onclick="() => PreheatAsync(h, b)" disabled="@(!IsConnected)"
title="@($"Hotend {hotend}°C / Bed {bed}°C")">
@label
</button>
}
</div>
}
@* Hotend *@
<div class="dash-temp-row mb-3">
<div class="d-flex align-items-center justify-content-between mb-1">
Expand All @@ -183,11 +197,14 @@
<div class="progress-bar @(Telemetry.HotendTemp > 150 ? "bg-danger" : Telemetry.HotendTemp > 60 ? "bg-warning" : "bg-secondary")"
style="width:@($"{Math.Min(Telemetry.HotendTemp / 300.0 * 100, 100):F0}%")"></div>
</div>
<div class="input-group input-group-sm">
<input type="number" min="0" max="300" class="form-control" @bind="hotendTarget" disabled="@(!IsConnected)" placeholder="Target °C">
<span class="input-group-text">°C</span>
<button class="btn btn-outline-danger" @onclick="SetHotendTempAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
@if (SupportsDirectControl)
{
<div class="input-group input-group-sm">
<input type="number" min="0" max="300" class="form-control" @bind="hotendTarget" disabled="@(!IsConnected)" placeholder="Target °C">
<span class="input-group-text">°C</span>
<button class="btn btn-outline-danger" @onclick="SetHotendTempAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
}
</div>
@* Bed *@
<div class="dash-temp-row mb-3">
Expand All @@ -204,11 +221,14 @@
<div class="progress mb-2" style="height:3px;">
<div class="progress-bar bg-warning" style="width:@($"{Math.Min(Telemetry.BedTemp / 120.0 * 100, 100):F0}%")"></div>
</div>
<div class="input-group input-group-sm">
<input type="number" min="0" max="120" class="form-control" @bind="bedTarget" disabled="@(!IsConnected)" placeholder="Target °C">
<span class="input-group-text">°C</span>
<button class="btn btn-outline-warning" @onclick="SetBedTempAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
@if (SupportsDirectControl)
{
<div class="input-group input-group-sm">
<input type="number" min="0" max="120" class="form-control" @bind="bedTarget" disabled="@(!IsConnected)" placeholder="Target °C">
<span class="input-group-text">°C</span>
<button class="btn btn-outline-warning" @onclick="SetBedTempAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
}
</div>
@* Chamber temp (shown when data is available from printer) *@
@if (Telemetry.ChamberTemp > 0 || Telemetry.ChamberTarget > 0)
Expand Down Expand Up @@ -241,16 +261,21 @@
<div class="progress mb-2" style="height:3px;">
<div class="progress-bar bg-info" style="width:@Telemetry.FanSpeed%"></div>
</div>
<div class="input-group input-group-sm">
<input type="number" min="0" max="100" class="form-control" @bind="fanTarget" disabled="@(!IsConnected)" placeholder="Fan %">
<span class="input-group-text">%</span>
<button class="btn btn-outline-info" @onclick="SetFanSpeedAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
@if (SupportsDirectControl)
{
<div class="input-group input-group-sm">
<input type="number" min="0" max="100" class="form-control" @bind="fanTarget" disabled="@(!IsConnected)" placeholder="Fan %">
<span class="input-group-text">%</span>
<button class="btn btn-outline-info" @onclick="SetFanSpeedAsync" disabled="@(!IsConnected)">@Localizer[Resources.ControlPanel_Set]</button>
</div>
}
</div>
</div>
</div>

@* Extrude *@
@* Extrude (direct-control backends only) *@
@if (SupportsDirectControl)
{
<div class="card mb-3">
<div class="card-header d-flex align-items-center gap-2 py-2">
<i class="bi bi-moisture text-secondary"></i>
Expand Down Expand Up @@ -283,6 +308,7 @@
</div>
</div>
</div>
}

@* Speed / Flow *@
<div class="card mb-3">
Expand All @@ -297,26 +323,34 @@
@Localizer[Resources.ControlPanel_PrintSpeed]
<span class="badge bg-primary ms-1">@Telemetry.FeedRate%</span>
</label>
<div class="input-group input-group-sm">
<input type="number" min="1" max="300" class="form-control" @bind="printSpeed" @bind:after="UpdatePrintSpeedAsync" disabled="@(!IsConnected)">
<span class="input-group-text">%</span>
</div>
@if (SupportsDirectControl)
{
<div class="input-group input-group-sm">
<input type="number" min="1" max="300" class="form-control" @bind="printSpeed" @bind:after="UpdatePrintSpeedAsync" disabled="@(!IsConnected)">
<span class="input-group-text">%</span>
</div>
}
</div>
<div class="col">
<label class="form-label small text-muted mb-1">
@Localizer[Resources.ControlPanel_PrintFlow]
<span class="badge bg-secondary ms-1">@Telemetry.FlowRate%</span>
</label>
<div class="input-group input-group-sm">
<input type="number" min="1" max="300" class="form-control" @bind="printFlow" @bind:after="UpdatePrintFlowAsync" disabled="@(!IsConnected)">
<span class="input-group-text">%</span>
</div>
@if (SupportsDirectControl)
{
<div class="input-group input-group-sm">
<input type="number" min="1" max="300" class="form-control" @bind="printFlow" @bind:after="UpdatePrintFlowAsync" disabled="@(!IsConnected)">
<span class="input-group-text">%</span>
</div>
}
</div>
</div>
</div>
</div>

@* Calibration *@
@* Calibration (direct-control backends only) *@
@if (SupportsDirectControl)
{
<div class="card">
<div class="card-header d-flex align-items-center gap-2 py-2">
<i class="bi bi-rulers text-secondary"></i>
Expand All @@ -326,6 +360,45 @@
<Calibration />
</div>
</div>
}

@* Printer Queue (Moonraker only) *@
@if (SupportsPrinterQueue)
{
<div class="card mt-3">
<div class="card-header d-flex align-items-center gap-2 py-2">
<i class="bi bi-collection-play text-primary"></i>
<span class="fw-semibold">@Localizer[Resources.ControlPanel_PrinterQueue]</span>
<button class="btn btn-sm btn-outline-secondary ms-auto" @onclick="SafeRefreshPrinterQueueAsync" disabled="@(!IsConnected)" title="@Localizer[Resources.PrintQueue_Refresh]">
<i class="bi bi-arrow-clockwise"></i>
</button>
</div>
<div class="card-body p-0">
@if (!_printerQueue.Any())
{
<div class="text-center text-muted py-3">
<i class="bi bi-inbox" style="font-size: 1.5rem;"></i>
<p class="mt-1 mb-0 small">@Localizer[Resources.ControlPanel_PrinterQueueEmpty]</p>
</div>
}
else
{
<div class="list-group list-group-flush">
@foreach (var job in _printerQueue)
{
<div class="list-group-item px-3 py-2">
<div class="d-flex align-items-center gap-2">
<i class="bi bi-file-earmark-code text-muted"></i>
<span class="text-truncate flex-grow-1 small fw-semibold" title="@job.FileName">@job.FileName</span>
<small class="text-muted flex-shrink-0">@DateTimeOffset.FromUnixTimeSeconds((long)job.TimeAdded).ToLocalTime().ToString("HH:mm")</small>
</div>
</div>
}
</div>
}
</div>
</div>
}
</div>

@* ── File storage ── *@
Expand All @@ -346,6 +419,11 @@
private int printSpeed = 100;
private int printFlow = 100;

private List<PrintQueueEntry> _printerQueue = [];

private bool SupportsDirectControl => PrinterServiceFactory.Current?.SupportsDirectControl ?? true;
private bool SupportsPrinterQueue => PrinterServiceFactory.Current?.SupportsPrinterQueue ?? false;

private static readonly (string Label, int Hotend, int Bed)[] PreheatProfiles =
[
("PLA", 200, 60),
Expand Down Expand Up @@ -460,6 +538,33 @@
return printer?.SetPrintFlow(printFlow) ?? Task.CompletedTask;
}

protected override void OnInitialized()
{
base.OnInitialized();
if (SupportsPrinterQueue)
_ = SafeRefreshPrinterQueueAsync();
}

protected override void HandleConnectionChanged(object? sender, bool connected)
{
base.HandleConnectionChanged(sender, connected);
if (connected && SupportsPrinterQueue)
_ = SafeRefreshPrinterQueueAsync();
}

private async Task RefreshPrinterQueueAsync()
{
if (PrinterServiceFactory.Current is { SupportsPrinterQueue: true } printer)
{
_printerQueue = await printer.GetPrinterQueueAsync();
await InvokeAsync(StateHasChanged);
}
}

// Wraps RefreshPrinterQueueAsync with error handling for fire-and-forget calls
private Task SafeRefreshPrinterQueueAsync() =>
RunAsync(RefreshPrinterQueueAsync, Localizer[Resources.ControlPanel_PrinterQueue]);

private readonly PrinterTelemetry fallbackTelemetry = new();
private PrinterTelemetry Telemetry => PrinterServiceFactory.Current?.LastTelemetry ?? fallbackTelemetry;
}
25 changes: 20 additions & 5 deletions MakerPrompt.Shared/Components/PrinterConnectionModal.razor
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@
<p class="mb-0">@localizer[Resources.NavConnection_DemoServiceDescription]</p>
</div>
}
else if (_editConnectionType == PrinterConnectionType.PrusaConnect)
{
<div class="alert alert-info mb-3">
<p class="mb-0">@localizer[Resources.NavConnect_PrusaConnectDescription]</p>
</div>
<div class="mb-3">
<label class="form-label">@localizer[Resources.NavPrinters_PrinterUuid]</label>
<input type="text" class="form-control" @bind="_editApiSettings.UserName" placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" />
</div>
<div class="mb-3">
<label class="form-label">@localizer[Resources.NavPrinters_ApiKey]</label>
<input type="password" class="form-control" @bind="_editApiSettings.Password" placeholder="Enter API key" />
</div>
}
else
{
<div class="mb-3">
Expand Down Expand Up @@ -165,11 +179,9 @@
private PrinterConnectionType _editConnectionType = PrinterConnectionType.Demo;
private List<string> _availablePorts = new();
private readonly List<int> BaudRates = [9600, 19200, 38400, 57600, 115200, 250000];
// PrusaConnect uses the fleet-picker flow (PrusaConnectProvider), not this modal.
// PrusaConnect uses UUID + Bearer token from the mobile API.
private static readonly PrinterConnectionType[] _connectionTypes =
Enum.GetValues<PrinterConnectionType>()
.Where(t => t != PrinterConnectionType.PrusaConnect)
.ToArray();
Enum.GetValues<PrinterConnectionType>().ToArray();

public async Task ShowAddAsync()
{
Expand Down Expand Up @@ -225,7 +237,10 @@
}

// Validate URL for API-based backends before attempting anything.
if (_editConnectionType != PrinterConnectionType.Serial && _editConnectionType != PrinterConnectionType.Demo
// PrusaConnect uses UUID + API key, not a URL — skip URL validation for it.
if (_editConnectionType != PrinterConnectionType.Serial
&& _editConnectionType != PrinterConnectionType.Demo
&& _editConnectionType != PrinterConnectionType.PrusaConnect
&& string.IsNullOrWhiteSpace(_editApiSettings.Url))
{
_connectionError = "URL is required. Enter the printer\'s IP address or hostname (e.g. http://192.168.1.100).";
Expand Down
18 changes: 18 additions & 0 deletions MakerPrompt.Shared/Infrastructure/IPrinterCommunicationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,29 @@ public interface IPrinterCommunicationService : IAsyncDisposable
bool IsConnected { get; }
bool IsPrinting { get; }

/// <summary>
/// True when the backend supports direct printer control commands
/// (temperature set, motion, extrusion, fan, etc.).
/// Monitoring-only backends (PrusaLink, PrusaConnect) return false.
/// </summary>
bool SupportsDirectControl => true;

/// <summary>
/// True when the backend exposes a queryable printer-side print queue.
/// Currently only Moonraker supports this.
/// </summary>
bool SupportsPrinterQueue => false;

Task<bool> ConnectAsync(PrinterConnectionSettings connectionSettings);
Task DisconnectAsync();
Task WriteDataAsync(string command);
Task<PrinterTelemetry> GetPrinterTelemetryAsync();
Task<List<FileEntry>> GetFilesAsync();
/// <summary>
/// Returns the printer-side print queue entries. Returns an empty list
/// for backends that do not support a print queue (<see cref="SupportsPrinterQueue"/> is false).
/// </summary>
Task<List<PrintQueueEntry>> GetPrinterQueueAsync() => Task.FromResult(new List<PrintQueueEntry>());
Task SetHotendTemp(int targetTemp = 0);
Task SetBedTemp(int targetTemp = 0);
Task Home(bool x = true, bool y = true, bool z = true);
Expand Down
2 changes: 1 addition & 1 deletion MakerPrompt.Shared/Layout/MainLayout.razor
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<span class="ms-2 fw-semibold text-body-secondary d-none d-md-inline">@ConfigService.Configuration.FarmName</span>
}
<div class="ms-auto">
<NavTop />
<NavPrinters />
</div>
</header>

Expand Down
6 changes: 6 additions & 0 deletions MakerPrompt.Shared/Layout/NavMenu.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
<span class="sidebar-label">@Resources.PageTitle_Fleet</span>
</NavLink>
</li>
<li class="nav-item">
<NavLink class="nav-link sidebar-link px-3 py-2 rounded" href="projecthub" Match="NavLinkMatch.Prefix">
<i class="bi bi-folder2-open sidebar-icon"></i>
<span class="sidebar-label">@Resources.PageTitle_ProjectHub</span>
</NavLink>
</li>
}
else
{
Expand Down
20 changes: 20 additions & 0 deletions MakerPrompt.Shared/Pages/ProjectHub.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
@page "/projecthub"
@inject IAppConfigurationService ConfigService
@inject NavigationManager Navigation

<LocalizedTitle TitleKey="@Resources.PageTitle_ProjectHub" />

<div class="container-fluid">
<PrintQueue />
</div>

@code {
protected override async Task OnInitializedAsync()
{
await ConfigService.InitializeAsync();
if (!ConfigService.Configuration.FarmModeEnabled)
{
Navigation.NavigateTo("/dashboard", replace: true);
}
}
}
Loading