Escl: Implement GetCaps

This commit is contained in:
Ben Olden-Cooligan 2024-08-07 18:17:58 -07:00
parent 53a1beb057
commit f569244b71
6 changed files with 121 additions and 36 deletions

View File

@ -39,6 +39,7 @@ internal class EsclApiController : WebApiController
new XElement(PwgNs + "Version", caps.Version),
new XElement(PwgNs + "MakeAndModel", caps.MakeAndModel),
new XElement(PwgNs + "SerialNumber", caps.SerialNumber),
new XElement(ScanNs + "Manufacturer", caps.Manufacturer),
new XElement(ScanNs + "UUID", caps.Uuid),
new XElement(ScanNs + "AdminURI", ""),
new XElement(ScanNs + "IconURI", iconUri),

View File

@ -31,6 +31,7 @@ internal static class CapabilitiesParser
Version = root.Element(PwgNs + "Version")?.Value ?? EsclCapabilities.DEFAULT_VERSION,
MakeAndModel = root.Element(PwgNs + "MakeAndModel")?.Value,
SerialNumber = root.Element(PwgNs + "SerialNumber")?.Value,
Manufacturer = root.Element(ScanNs + "Manufacturer")?.Value,
Uuid = root.Element(ScanNs + "UUID")?.Value,
AdminUri = root.Element(ScanNs + "AdminURI")?.Value,
IconUri = root.Element(ScanNs + "IconURI")?.Value,

View File

@ -7,6 +7,7 @@ public class EsclCapabilities
public string Version { get; init; } = DEFAULT_VERSION;
public string? MakeAndModel { get; init; }
public string? SerialNumber { get; init; }
public string? Manufacturer { get; init; }
public string? Uuid { get; init; }
public string? AdminUri { get; init; }
public string? IconUri { get; init; }

View File

@ -1,3 +1,4 @@
using System.Collections.Immutable;
using System.Net.Http;
using System.Net.Sockets;
using System.Threading;
@ -53,9 +54,6 @@ internal class EsclScanDriver : IScanDriver
{
return;
}
// TODO: When we implement scanner capabilities, store all the connection information in there so we can
// try and connect directly before querying for a potentially-updated-IP (and then back-propagate the new
// connection info).
var id = service.Uuid;
var name = string.IsNullOrEmpty(service.ScannerName)
? $"{ip}"
@ -73,37 +71,100 @@ internal class EsclScanDriver : IScanDriver
}
}
public Task<ScanCaps> GetCaps(ScanOptions options, CancellationToken cancelToken)
public async Task<ScanCaps> GetCaps(ScanOptions options, CancellationToken cancelToken)
{
return Task.FromResult(new ScanCaps());
if (cancelToken.IsCancellationRequested) return new ScanCaps();
var client = await GetEsclClient(options, cancelToken);
if (client == null) return new ScanCaps();
try
{
var caps = await client.GetCapabilities();
return new ScanCaps
{
MetadataCaps = new()
{
Model = caps.MakeAndModel,
Manufacturer = caps.Manufacturer,
SerialNumber = caps.SerialNumber,
IconUri = client.IconUri
},
PaperSourceCaps = new()
{
SupportsFlatbed = caps.PlatenCaps != null,
SupportsFeeder = caps.AdfSimplexCaps != null,
SupportsDuplex = caps.AdfDuplexCaps != null,
CanCheckIfFeederHasPaper = true
},
FlatbedCaps = MapCaps(caps.PlatenCaps),
FeederCaps = MapCaps(caps.AdfSimplexCaps),
DuplexCaps = MapCaps(caps.AdfDuplexCaps)
};
}
catch (HttpRequestException ex) when (ex.InnerException is TaskCanceledException or SocketException)
{
// A connection timeout manifests as TaskCanceledException
_logger.LogError(ex, "Error connecting to ESCL device");
throw new DeviceCommunicationException();
}
catch (TaskCanceledException)
{
}
return new ScanCaps();
}
private PerSourceCaps? MapCaps(EsclInputCaps? caps)
{
if (caps == null)
{
return null;
}
return PerSourceCaps.UnionAll(caps.SettingProfiles.Select(profile => MapSettingProfile(caps, profile)));
}
private PerSourceCaps MapSettingProfile(EsclInputCaps caps, EsclSettingProfile profile)
{
DpiCaps? dpiCaps = null;
if (profile.DiscreteResolutions.Count > 0)
{
dpiCaps = new DpiCaps
{
Values = profile.DiscreteResolutions
.Where(res => res.XResolution == res.YResolution)
.Select(res => res.XResolution).ToImmutableList()
};
}
else if (profile.XResolutionRange != null && profile.YResolutionRange != null)
{
int min = Math.Max(profile.XResolutionRange.Min, profile.YResolutionRange.Min);
int max = Math.Min(profile.XResolutionRange.Max, profile.YResolutionRange.Max);
int step = Math.Max(profile.XResolutionRange.Step, profile.YResolutionRange.Step);
dpiCaps = DpiCaps.ForRange(min, max, step);
}
return new PerSourceCaps
{
DpiCaps = dpiCaps,
BitDepthCaps = new BitDepthCaps
{
SupportsColor = profile.ColorModes.Contains(EsclColorMode.RGB24),
SupportsGrayscale = profile.ColorModes.Contains(EsclColorMode.Grayscale8),
SupportsBlackAndWhite = profile.ColorModes.Contains(EsclColorMode.BlackAndWhite1)
},
PageSizeCaps = caps.MaxWidth != null && caps.MaxHeight != null
? new PageSizeCaps
{
ScanArea = new PageSize(caps.MaxWidth.Value / 300m, caps.MaxHeight.Value / 300m, PageSizeUnit.Inch)
}
: null
};
}
public async Task Scan(ScanOptions options, CancellationToken cancelToken, IScanEvents scanEvents,
Action<IMemoryImage> callback)
{
if (cancelToken.IsCancellationRequested) return;
EsclClient client;
string deviceId = options.Device!.ID;
if (deviceId.StartsWith("http://") || deviceId.StartsWith("https://"))
{
client = new EsclClient(new Uri(deviceId));
// TODO: Handle device offline?
}
else
{
var service = await FindDeviceEsclService(options, cancelToken);
if (cancelToken.IsCancellationRequested) return;
if (service == null) throw new DeviceOfflineException();
client = new EsclClient(service);
}
client.SecurityPolicy = options.EsclOptions.SecurityPolicy;
client.Logger = _logger;
client.CancelToken = cancelToken;
var client = await GetEsclClient(options, cancelToken);
if (client == null) return;
try
{
@ -169,6 +230,31 @@ internal class EsclScanDriver : IScanDriver
}
}
private async Task<EsclClient?> GetEsclClient(ScanOptions options, CancellationToken cancelToken)
{
EsclClient client;
string deviceId = options.Device!.ID;
if (deviceId.StartsWith("http://") || deviceId.StartsWith("https://"))
{
client = new EsclClient(new Uri(deviceId));
// TODO: Handle device offline?
}
else
{
var service = await FindDeviceEsclService(options, cancelToken);
if (cancelToken.IsCancellationRequested) return null;
if (service == null) throw new DeviceOfflineException();
client = new EsclClient(service);
}
client.SecurityPolicy = options.EsclOptions.SecurityPolicy;
client.Logger = _logger;
client.CancelToken = cancelToken;
return client;
}
private async Task<EsclJob> CreateScanJobAndCorrectInvalidSettings(EsclClient client, EsclScanSettings scanSettings)
{
_logger.LogDebug("Creating ESCL job: format {Format}, source {Source}, mode {Mode}",

View File

@ -25,11 +25,6 @@ public class MetadataCaps
/// </summary>
public string? SerialNumber { get; init; }
/// <summary>
/// The location note associated with the device.
/// </summary>
public string? Location { get; init; }
/// <summary>
/// The URI for an icon associated with the device.
/// </summary>

View File

@ -11,10 +11,11 @@ public class PerSourceCaps
/// Gets an object representing the union of all possible option values allowed by the provided objects.
/// This can be helpful when presenting the user with a single set of possible options for multiple sources.
/// </summary>
public static PerSourceCaps UnionAll(ICollection<PerSourceCaps> caps)
public static PerSourceCaps UnionAll(IEnumerable<PerSourceCaps> caps)
{
var capsColl = caps as ICollection<PerSourceCaps> ?? caps.ToList();
DpiCaps? dpiCaps = null;
foreach (var dpiValues in caps.Select(x => x.DpiCaps?.Values).WhereNotNull())
foreach (var dpiValues in capsColl.Select(x => x.DpiCaps?.Values).WhereNotNull())
{
dpiCaps = new DpiCaps
{
@ -22,7 +23,7 @@ public class PerSourceCaps
};
}
BitDepthCaps? bitDepthCaps = null;
foreach (var bd in caps.Select(x => x.BitDepthCaps).WhereNotNull())
foreach (var bd in capsColl.Select(x => x.BitDepthCaps).WhereNotNull())
{
bitDepthCaps = new BitDepthCaps
{
@ -32,7 +33,7 @@ public class PerSourceCaps
};
}
PageSizeCaps? pageSizeCaps = null;
foreach (var area in caps.Select(x => x.PageSizeCaps?.ScanArea).WhereNotNull())
foreach (var area in capsColl.Select(x => x.PageSizeCaps?.ScanArea).WhereNotNull())
{
pageSizeCaps = new PageSizeCaps
{