mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 16:01:35 -05:00
A server Auto Nest showed only "Nesting on the nest server" for however long the job took, and Stop could take a whole 20-second heartbeat to reach the worker. Now the desktop shows the same Best plan so far strip a local run does, and a job that ends while OpenNest is in the background says so. - Worker: RecordingNestingEngine taps each solve's progress. NestJobReporter stores only the latest stage and plan, at most once every two seconds, so a chatty engine cannot flood SQLite. Each round also reads the cancel flag, so Stop reaches the worker within one interval (about 2 s in the Docker smoke test). - Server: schema v16 adds per-job revision, progress, plan and planRevision columns, which the upgrade adds only where missing. PUT .../progress and .../plan renew the lease like a heartbeat. GET .../events is Server-Sent Events, woken by an in-process change signal: plan and job events with revision ids, keep-alives, Last-Event-ID resume, and the final state always sent before the stream ends. GET .../plan serves the polling fallback. Progress goes over HTTP because the worker never opens the database (another host, NFS). - Desktop: NestJobWatch follows the stream. When the stream drops it polls once and reconnects, and it polls throughout when the server has no stream. RemoteNestPlanPreview draws worker plans from the client's own request, including cutout-trial frame proxies. ServerNestNotice shows a tray balloon (a toast on Windows 10/11) when the job ends while OpenNest is not in front. A real toast would need an AUMID shortcut that Velopack does not promise. Clicking the balloon brings the review forward; nothing is kept without Keep. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
106 lines
4.7 KiB
C#
106 lines
4.7 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.Json;
|
|
using System.Text.Json.Serialization;
|
|
|
|
namespace OpenNest.Engine.Jobs.Remote;
|
|
|
|
/// <summary>
|
|
/// Every engine solve one <see cref="NestPipeline.Run(INestingEngine, string, NestPipelineRequest, IProgress{NestJobProgress}, System.Threading.CancellationToken)"/>
|
|
/// made, in order: what it asked (part and stock ids with their quantities) and the raw result
|
|
/// the engine returned. A remote solve sends this back instead of bound plates, so the client
|
|
/// replays the pipeline against its own drawings and validates and binds the result itself
|
|
/// (<see cref="RemoteNestSubmission"/>). Poses are kept bit for bit, nonfinite ones included,
|
|
/// so the client's validation sees exactly what the engine returned.
|
|
/// </summary>
|
|
public sealed class NestSolveTranscript
|
|
{
|
|
public const int CurrentVersion = 1;
|
|
|
|
internal static readonly JsonSerializerOptions Options = new()
|
|
{
|
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
|
PropertyNameCaseInsensitive = true,
|
|
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals,
|
|
Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) },
|
|
};
|
|
|
|
public int Version { get; set; } = CurrentVersion;
|
|
public List<SolveJson> Solves { get; set; } = new();
|
|
|
|
public string ToJson() => JsonSerializer.Serialize(this, Options);
|
|
|
|
/// <summary>Reads a transcript; throws <see cref="FormatException"/> when it is not one this build understands.</summary>
|
|
public static NestSolveTranscript FromJson(string json)
|
|
{
|
|
NestSolveTranscript transcript;
|
|
try { transcript = JsonSerializer.Deserialize<NestSolveTranscript>(json, Options); }
|
|
catch (JsonException ex) { throw new FormatException("The server's nest result is not valid JSON.", ex); }
|
|
if (transcript?.Solves == null || transcript.Solves.Any(s => s?.Result == null))
|
|
throw new FormatException("The server's nest result is incomplete.");
|
|
if (transcript.Version != CurrentVersion)
|
|
throw new FormatException($"The server's nest result is version {transcript.Version}; this build reads {CurrentVersion}.");
|
|
return transcript;
|
|
}
|
|
|
|
internal void Add(NestJob job, NestJobResult result) =>
|
|
Solves.Add(new SolveJson { Request = Fingerprint(job), Result = ResultJson.From(result) });
|
|
|
|
/// <summary>What a solve was asked: the job's part and stock ids with their quantities, in order.</summary>
|
|
internal static List<string> Fingerprint(NestJob job) =>
|
|
job.Parts.Select(p => $"part:{p.Id}:{p.Quantity}")
|
|
.Concat(job.Plates.Select(s => $"stock:{s.Id}:{s.Quantity?.ToString() ?? "*"}"))
|
|
.ToList();
|
|
|
|
public sealed class SolveJson
|
|
{
|
|
public List<string> Request { get; set; } = new();
|
|
public ResultJson Result { get; set; }
|
|
}
|
|
|
|
public sealed class ResultJson
|
|
{
|
|
public NestJobStatus Status { get; set; }
|
|
public NestJobStopReason StopReason { get; set; }
|
|
public List<PlateJson> Plates { get; set; } = new();
|
|
public List<PartFulfillment> Fulfillment { get; set; } = new();
|
|
public List<StockUsage> StockUsage { get; set; } = new();
|
|
|
|
internal static ResultJson From(NestJobResult result) => new()
|
|
{
|
|
Status = result.Status,
|
|
StopReason = result.StopReason,
|
|
Plates = result.Plates.Select(p => new PlateJson
|
|
{
|
|
PlateIndex = p.PlateIndex,
|
|
Stock = NestStockJson.From(p.Stock),
|
|
Placements = p.Placements.ToList(),
|
|
}).ToList(),
|
|
Fulfillment = result.Fulfillment.ToList(),
|
|
StockUsage = result.StockUsage.ToList(),
|
|
};
|
|
|
|
/// <summary>The raw result, with each sheet that matches one of <paramref name="job"/>'s
|
|
/// stock entries using that entry itself, as an engine solving the job would.</summary>
|
|
internal NestJobResult ToResult(NestJob job)
|
|
{
|
|
var plates = new List<NestJobPlateResult>(Plates.Count);
|
|
foreach (var plate in Plates)
|
|
{
|
|
var stock = plate.Stock?.ToStock() ?? throw new FormatException("A result sheet has no stock.");
|
|
var own = job.Plates.FirstOrDefault(s => RepeatedPlateGrouping.SameStock(s, stock));
|
|
plates.Add(new NestJobPlateResult(plate.PlateIndex, own ?? stock, plate.Placements ?? new()));
|
|
}
|
|
return new NestJobResult(Status, StopReason, plates, Fulfillment ?? new(), StockUsage ?? new());
|
|
}
|
|
}
|
|
|
|
public sealed class PlateJson
|
|
{
|
|
public int PlateIndex { get; set; }
|
|
public NestStockJson Stock { get; set; }
|
|
public List<NestJobPlacement> Placements { get; set; } = new();
|
|
}
|
|
}
|