mirror of
https://github.com/cyanfish/naps2.git
synced 2024-09-11 15:26:55 +03:00
Escl: Server job state management and ADF support
This commit is contained in:
parent
c61c2babe7
commit
9e80eafea2
@ -1,9 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
using System.Reflection;
|
||||
using System.Xml.Linq;
|
||||
using EmbedIO;
|
||||
using EmbedIO.Routing;
|
||||
using EmbedIO.WebApi;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace NAPS2.Escl.Server;
|
||||
|
||||
@ -14,11 +14,13 @@ internal class EsclApiController : WebApiController
|
||||
|
||||
private readonly EsclDeviceConfig _deviceConfig;
|
||||
private readonly EsclServerState _serverState;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
internal EsclApiController(EsclDeviceConfig deviceConfig, EsclServerState serverState)
|
||||
internal EsclApiController(EsclDeviceConfig deviceConfig, EsclServerState serverState, ILogger logger)
|
||||
{
|
||||
_deviceConfig = deviceConfig;
|
||||
_serverState = serverState;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[Route(HttpVerbs.Get, "/ScannerCapabilities")]
|
||||
@ -37,44 +39,55 @@ internal class EsclApiController : WebApiController
|
||||
new XElement(ScanNs + "IconURI", iconUri),
|
||||
new XElement(ScanNs + "Naps2Extensions", "Progress"),
|
||||
new XElement(ScanNs + "Platen",
|
||||
new XElement(ScanNs + "PlatenInputCaps",
|
||||
new XElement(ScanNs + "MinWidth", "1"),
|
||||
new XElement(ScanNs + "MaxWidth", "3000"),
|
||||
new XElement(ScanNs + "MinHeight", "1"),
|
||||
new XElement(ScanNs + "MaxHeight", "3600"),
|
||||
new XElement(ScanNs + "MaxScanRegions", "1"),
|
||||
new XElement(ScanNs + "SettingProfiles",
|
||||
new XElement(ScanNs + "SettingProfile",
|
||||
new XElement(ScanNs + "ColorModes",
|
||||
new XElement(ScanNs + "ColorMode", "BlackAndWhite1"),
|
||||
new XElement(ScanNs + "ColorMode", "Grayscale8"),
|
||||
new XElement(ScanNs + "ColorMode", "RGB24")),
|
||||
new XElement(ScanNs + "DocumentFormats",
|
||||
new XElement(PwgNs + "DocumentFormat", "application/pdf"),
|
||||
new XElement(PwgNs + "DocumentFormat", "image/jpeg"),
|
||||
new XElement(PwgNs + "DocumentFormat", "image/png"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "application/pdf"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "image/jpeg"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "image/png")
|
||||
),
|
||||
new XElement(ScanNs + "SupportedResolutions",
|
||||
new XElement(ScanNs + "DiscreteResolutions",
|
||||
CreateResolution(100),
|
||||
CreateResolution(150),
|
||||
CreateResolution(200),
|
||||
CreateResolution(300),
|
||||
CreateResolution(400),
|
||||
CreateResolution(600),
|
||||
CreateResolution(800),
|
||||
CreateResolution(1200),
|
||||
CreateResolution(2400),
|
||||
CreateResolution(4800)
|
||||
))))))));
|
||||
new XElement(ScanNs + "PlatenInputCaps", GetCommonInputCaps())),
|
||||
new XElement(ScanNs + "Adf",
|
||||
new XElement(ScanNs + "AdfSimplexInputCaps", GetCommonInputCaps()),
|
||||
new XElement(ScanNs + "AdfDuplexInputCaps", GetCommonInputCaps()))));
|
||||
Response.ContentType = "text/xml";
|
||||
using var writer = new StreamWriter(HttpContext.OpenResponseStream());
|
||||
await writer.WriteAsync(doc);
|
||||
}
|
||||
|
||||
private object[] GetCommonInputCaps()
|
||||
{
|
||||
// TODO: After implementing scanner capabilities this should be scanner-specific
|
||||
return
|
||||
[
|
||||
new XElement(ScanNs + "MinWidth", "1"),
|
||||
new XElement(ScanNs + "MaxWidth", "3000"),
|
||||
new XElement(ScanNs + "MinHeight", "1"),
|
||||
new XElement(ScanNs + "MaxHeight", "3600"),
|
||||
new XElement(ScanNs + "MaxScanRegions", "1"),
|
||||
new XElement(ScanNs + "SettingProfiles",
|
||||
new XElement(ScanNs + "SettingProfile",
|
||||
new XElement(ScanNs + "ColorModes",
|
||||
new XElement(ScanNs + "ColorMode", "BlackAndWhite1"),
|
||||
new XElement(ScanNs + "ColorMode", "Grayscale8"),
|
||||
new XElement(ScanNs + "ColorMode", "RGB24")),
|
||||
new XElement(ScanNs + "DocumentFormats",
|
||||
new XElement(PwgNs + "DocumentFormat", "application/pdf"),
|
||||
new XElement(PwgNs + "DocumentFormat", "image/jpeg"),
|
||||
new XElement(PwgNs + "DocumentFormat", "image/png"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "application/pdf"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "image/jpeg"),
|
||||
new XElement(ScanNs + "DocumentFormatExt", "image/png")
|
||||
),
|
||||
new XElement(ScanNs + "SupportedResolutions",
|
||||
new XElement(ScanNs + "DiscreteResolutions",
|
||||
CreateResolution(100),
|
||||
CreateResolution(150),
|
||||
CreateResolution(200),
|
||||
CreateResolution(300),
|
||||
CreateResolution(400),
|
||||
CreateResolution(600),
|
||||
CreateResolution(800),
|
||||
CreateResolution(1200),
|
||||
CreateResolution(2400),
|
||||
CreateResolution(4800)
|
||||
))))
|
||||
];
|
||||
}
|
||||
|
||||
private XElement CreateResolution(int res) =>
|
||||
new(ScanNs + "DiscreteResolution",
|
||||
new XElement(ScanNs + "XResolution", res.ToString()),
|
||||
@ -100,26 +113,29 @@ internal class EsclApiController : WebApiController
|
||||
public async Task GetScannerStatus()
|
||||
{
|
||||
var jobsElement = new XElement(ScanNs + "Jobs");
|
||||
foreach (var jobState in _serverState.Jobs.Values)
|
||||
foreach (var jobInfo in _serverState.Jobs.Values)
|
||||
{
|
||||
jobsElement.Add(new XElement(ScanNs + "JobInfo",
|
||||
new XElement(PwgNs + "JobUri", $"/eSCL/ScanJobs/{jobState.Id}"),
|
||||
new XElement(PwgNs + "JobUuid", jobState.Id),
|
||||
new XElement(ScanNs + "Age", Math.Ceiling(jobState.LastUpdated.Elapsed.TotalSeconds)),
|
||||
new XElement(PwgNs + "JobUri", $"/eSCL/ScanJobs/{jobInfo.Id}"),
|
||||
new XElement(PwgNs + "JobUuid", jobInfo.Id),
|
||||
new XElement(ScanNs + "Age", Math.Ceiling(jobInfo.LastUpdated.Elapsed.TotalSeconds)),
|
||||
// TODO: real data
|
||||
new XElement(PwgNs + "ImagesCompleted",
|
||||
jobState.Status is JobStatus.Pending or JobStatus.Processing ? "0" : "1"),
|
||||
jobInfo.State is EsclJobState.Pending or EsclJobState.Processing ? "0" : "1"),
|
||||
new XElement(PwgNs + "ImagesToTransfer", "1"),
|
||||
new XElement(PwgNs + "JobState", jobState.Status.ToString()),
|
||||
new XElement(PwgNs + "JobState", jobInfo.State.ToString()),
|
||||
new XElement(PwgNs + "JobStateReasons",
|
||||
new XElement(PwgNs + "JobStateReason",
|
||||
jobState.Status == JobStatus.Processing ? "JobScanning" : "JobCompletedSuccessfully"))));
|
||||
jobInfo.State == EsclJobState.Processing ? "JobScanning" : "JobCompletedSuccessfully"))));
|
||||
}
|
||||
var scannerState = _serverState.IsProcessing ? EsclScannerState.Processing : EsclScannerState.Idle;
|
||||
var adfState = _serverState.IsProcessing ? EsclAdfState.ScannerAdfProcessing : EsclAdfState.ScannedAdfLoaded;
|
||||
var doc =
|
||||
EsclXmlHelper.CreateDocAsString(
|
||||
new XElement(ScanNs + "ScannerStatus",
|
||||
new XElement(PwgNs + "Version", "2.6"),
|
||||
new XElement(PwgNs + "State", _serverState.IsProcessing ? "Processing" : "Idle"),
|
||||
new XElement(PwgNs + "State", scannerState),
|
||||
new XElement(ScanNs + "AdfState", adfState),
|
||||
jobsElement
|
||||
));
|
||||
Response.ContentType = "text/xml";
|
||||
@ -148,7 +164,7 @@ internal class EsclApiController : WebApiController
|
||||
return;
|
||||
}
|
||||
_serverState.IsProcessing = true;
|
||||
var jobState = JobState.CreateNewJob(_serverState, _deviceConfig.CreateJob(settings));
|
||||
var jobState = JobInfo.CreateNewJob(_serverState, _deviceConfig.CreateJob(settings));
|
||||
_serverState.Jobs[jobState.Id] = jobState;
|
||||
Response.Headers.Add("Location", $"{Request.Url}/{jobState.Id}");
|
||||
Response.StatusCode = 201; // Created
|
||||
@ -158,7 +174,7 @@ internal class EsclApiController : WebApiController
|
||||
public void CancelScanJob(string jobId)
|
||||
{
|
||||
if (_serverState.Jobs.TryGetValue(jobId, out var jobState) &&
|
||||
jobState.Status is JobStatus.Pending or JobStatus.Processing)
|
||||
jobState.State is EsclJobState.Pending or EsclJobState.Processing)
|
||||
{
|
||||
jobState.Job.Cancel();
|
||||
}
|
||||
@ -177,7 +193,7 @@ internal class EsclApiController : WebApiController
|
||||
public async Task Progress(string jobId)
|
||||
{
|
||||
if (_serverState.Jobs.TryGetValue(jobId, out var jobState) &&
|
||||
jobState.Status is JobStatus.Pending or JobStatus.Processing)
|
||||
jobState.State is EsclJobState.Pending or EsclJobState.Processing)
|
||||
{
|
||||
SetChunkedResponse();
|
||||
using var stream = Response.OutputStream;
|
||||
@ -193,9 +209,20 @@ internal class EsclApiController : WebApiController
|
||||
public async Task NextDocument(string jobId)
|
||||
{
|
||||
if (_serverState.Jobs.TryGetValue(jobId, out var jobState) &&
|
||||
jobState.Status is JobStatus.Pending or JobStatus.Processing)
|
||||
jobState.State is EsclJobState.Pending or EsclJobState.Processing)
|
||||
{
|
||||
if (await jobState.Job.WaitForNextDocument())
|
||||
bool result;
|
||||
try
|
||||
{
|
||||
result = await jobState.Job.WaitForNextDocument();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogDebug(ex, "ESCL server error waiting for document");
|
||||
Response.StatusCode = 500;
|
||||
return;
|
||||
}
|
||||
if (result)
|
||||
{
|
||||
Response.Headers.Add("Content-Location", $"/eSCL/ScanJobs/{jobState.Id}/1");
|
||||
SetChunkedResponse();
|
||||
@ -206,7 +233,7 @@ internal class EsclApiController : WebApiController
|
||||
}
|
||||
else
|
||||
{
|
||||
jobState.Status = JobStatus.Completed;
|
||||
jobState.State = EsclJobState.Completed;
|
||||
Response.StatusCode = 404;
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +74,7 @@ public class EsclServer : IEsclServer
|
||||
.WithMode(HttpListenerMode.EmbedIO)
|
||||
.WithUrlPrefix(url))
|
||||
.HandleUnhandledException(UnhandledServerException)
|
||||
.WithWebApi("/eSCL", m => m.WithController(() => new EsclApiController(deviceConfig, serverState)));
|
||||
.WithWebApi("/eSCL", m => m.WithController(() => new EsclApiController(deviceConfig, serverState, Logger)));
|
||||
await server.StartAsync(cancelToken);
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,5 @@ internal class EsclServerState
|
||||
{
|
||||
public bool IsProcessing { get; set; }
|
||||
|
||||
public Dictionary<string, JobState> Jobs { get; } = new();
|
||||
public Dictionary<string, JobInfo> Jobs { get; } = new();
|
||||
}
|
@ -2,14 +2,14 @@ using System.Diagnostics;
|
||||
|
||||
namespace NAPS2.Escl.Server;
|
||||
|
||||
internal class JobState
|
||||
internal class JobInfo
|
||||
{
|
||||
public static JobState CreateNewJob(EsclServerState serverState, IEsclScanJob job)
|
||||
public static JobInfo CreateNewJob(EsclServerState serverState, IEsclScanJob job)
|
||||
{
|
||||
var state = new JobState
|
||||
var state = new JobInfo
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("D"),
|
||||
Status = JobStatus.Processing,
|
||||
State = EsclJobState.Processing,
|
||||
LastUpdated = Stopwatch.StartNew(),
|
||||
Job = job
|
||||
};
|
||||
@ -17,12 +17,12 @@ internal class JobState
|
||||
{
|
||||
if (transition == StatusTransition.CancelJob)
|
||||
{
|
||||
state.Status = JobStatus.Canceled;
|
||||
state.State = EsclJobState.Canceled;
|
||||
state.LastUpdated = Stopwatch.StartNew();
|
||||
}
|
||||
if (transition == StatusTransition.AbortJob)
|
||||
{
|
||||
state.Status = JobStatus.Aborted;
|
||||
state.State = EsclJobState.Aborted;
|
||||
state.LastUpdated = Stopwatch.StartNew();
|
||||
}
|
||||
if (transition == StatusTransition.DeviceIdle)
|
||||
@ -35,7 +35,7 @@ internal class JobState
|
||||
|
||||
public required string Id { get; init; }
|
||||
|
||||
public required JobStatus Status { get; set; }
|
||||
public required EsclJobState State { get; set; }
|
||||
|
||||
public required Stopwatch LastUpdated { get; set; }
|
||||
|
@ -44,10 +44,21 @@ public class EsclClient
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected root element: " + doc.Root?.Name);
|
||||
}
|
||||
var jobStates = new Dictionary<string, EsclJobState>();
|
||||
foreach (var jobInfoEl in root.Element(ScanNs + "Jobs")?.Elements(ScanNs + "JobInfo") ?? [])
|
||||
{
|
||||
var jobUri = jobInfoEl.Element(PwgNs + "JobUri")?.Value;
|
||||
var jobState = ParseHelper.MaybeParseEnum(jobInfoEl.Element(PwgNs + "JobState"), EsclJobState.Unknown);
|
||||
if (jobUri != null && jobState != EsclJobState.Unknown)
|
||||
{
|
||||
jobStates.Add(jobUri, jobState);
|
||||
}
|
||||
}
|
||||
return new EsclScannerStatus
|
||||
{
|
||||
State = ParseHelper.MaybeParseEnum(root.Element(PwgNs + "State"), EsclScannerState.Unknown),
|
||||
AdfState = ParseHelper.MaybeParseEnum(root.Element(ScanNs + "AdfState"), EsclAdfState.Unknown)
|
||||
AdfState = ParseHelper.MaybeParseEnum(root.Element(ScanNs + "AdfState"), EsclAdfState.Unknown),
|
||||
JobStates = jobStates
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
namespace NAPS2.Escl.Server;
|
||||
namespace NAPS2.Escl;
|
||||
|
||||
public enum JobStatus
|
||||
public enum EsclJobState
|
||||
{
|
||||
Unknown,
|
||||
Pending,
|
||||
Processing,
|
||||
Completed,
|
@ -4,4 +4,5 @@ public class EsclScannerStatus
|
||||
{
|
||||
public EsclScannerState State { get; init; }
|
||||
public EsclAdfState AdfState { get; init; }
|
||||
public Dictionary<string, EsclJobState> JobStates { get; set; } = new();
|
||||
}
|
Loading…
Reference in New Issue
Block a user