naps2/NAPS2.Escl.Usb/EsclUsbContext.cs
2024-01-12 19:25:41 -08:00

160 lines
5.5 KiB
C#

using System.Net;
using System.Net.Sockets;
using System.Text;
using LibUsbDotNet;
using LibUsbDotNet.Info;
using LibUsbDotNet.LibUsb;
using LibUsbDotNet.Main;
using NAPS2.Escl.Client;
namespace NAPS2.Escl.Usb;
public class EsclUsbContext : IDisposable
{
private readonly UsbContext _usbContext = new();
private readonly UsbDevice _device;
private readonly List<UsbInterfaceInfo> _interfaces;
private readonly UsbEndpointInfo _bulkOut;
private readonly UsbEndpointInfo _bulkIn;
private readonly CancellationTokenSource _cts = new();
private TcpListener? _proxyListener;
private EsclClient? _client;
public EsclUsbContext(EsclUsbDescriptor descriptor)
{
_device = FindDevice(descriptor);
_interfaces = _device.ActiveConfigDescriptor.Interfaces.Where(EsclUsbPoller.IsIppInterface).ToList();
_bulkOut = _interfaces[0].Endpoints.First(x => x.Attributes == 2 && (x.EndpointAddress & 0x80) == 0);
_bulkIn = _interfaces[0].Endpoints.First(x => x.Attributes == 2 && (x.EndpointAddress & 0x80) == 0x80);
}
public void ConnectToDevice()
{
// Claim the port first as we want to fail before opening USB connections if needed
if (_proxyListener == null)
{
_proxyListener = new TcpListener(IPAddress.Loopback, 0);
_proxyListener.Start();
}
_device.Open();
foreach (var interfaceInfo in _interfaces)
{
_device.ClaimInterface(interfaceInfo.Number);
_device.SetAltInterface(interfaceInfo.AlternateSetting);
}
var port = ((IPEndPoint) _proxyListener.LocalEndpoint).Port;
_client = new EsclClient(new EsclService
{
IpV4 = IPAddress.Loopback,
IpV6 = null,
Host = IPAddress.Loopback.ToString(),
RemoteEndpoint = IPAddress.Loopback,
Port = port,
RootUrl = "eSCL",
Tls = false,
Uuid = Guid.Empty.ToString("D")
});
Task.Run(ProxyLoop);
}
private async Task ProxyLoop()
{
while (!_cts.IsCancellationRequested)
{
// TODO: Verify this works with cancellation (net462 doesn't have an overload that takes a token but we do call Stop so that might do it)
using var tcpClient = await _proxyListener!.AcceptTcpClientAsync();
// TODO: Handle exceptions
using var tcpStream = tcpClient.GetStream();
var requestStream = CopyFromNetworkStream(tcpStream);
var requestText = StreamToString(requestStream);
WriteToDevice(requestStream);
var responseStream = ReadFromDevice();
var responseText = StreamToString(responseStream);
CopyToNetworkStream(tcpStream, responseStream);
}
}
private string StreamToString(MemoryStream stream)
{
return Encoding.UTF8.GetString(stream.GetBuffer().Take((int) stream.Length).ToArray());
}
private MemoryStream CopyFromNetworkStream(NetworkStream tcpStream)
{
var stream = new MemoryStream();
var buffer = new byte[65_536];
tcpStream.ReadTimeout = 50;
try
{
while (tcpStream.Read(buffer, 0, buffer.Length) is var bytesRead and > 0)
{
stream.Write(buffer, 0, bytesRead);
}
}
catch (Exception)
{
if (stream.Length == 0) throw;
}
return stream;
}
private static void CopyToNetworkStream(NetworkStream tcpStream, MemoryStream stream)
{
tcpStream.Write(stream.GetBuffer(), 0, (int) stream.Length);
}
private MemoryStream ReadFromDevice()
{
var stream = new MemoryStream();
var buffer = new byte[65_536];
var reader = _device.OpenEndpointReader((ReadEndpointID) _bulkIn.EndpointAddress, buffer.Length);
// TODO: Do we need to handle longer timeouts and/or detect the end of stream based on content? e.g. if a chunked response
while (reader.Read(buffer, 0, buffer.Length, 1000, out var bytesRead) == Error.Success)
{
stream.Write(buffer, 0, bytesRead);
}
return stream;
}
private void WriteToDevice(MemoryStream memoryStream)
{
var writer = _device.OpenEndpointWriter((WriteEndpointID) _bulkOut.EndpointAddress);
// TODO: Do we need to handle incomplete transfers?
var status = writer.Write(memoryStream.GetBuffer(), 0, (int) memoryStream.Length, 1000, out var _);
if (status != Error.Success)
{
// Do something
}
}
public EsclClient Client => _client ??
throw new InvalidOperationException(
"You must call ConnectToDevice before getting the Client.");
private UsbDevice FindDevice(EsclUsbDescriptor descriptor)
{
foreach (var device in _usbContext.List())
{
if (device.Info.ProductId == descriptor.ProductId &&
device.Info.VendorId == descriptor.VendorId)
{
// TODO: Also check serial number? Would need to open device
// && device.Info.SerialNumber == descriptor.SerialNumber
return (UsbDevice) device;
}
device.Dispose();
}
throw new Exception($"ESCL USB device not found: {descriptor.Manufacturer} {descriptor.Product}");
}
public void Dispose()
{
_cts.Cancel();
_proxyListener?.Stop();
_device.Dispose();
_usbContext.Dispose();
}
}