Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

using System;
using System.Buffers;
#if !SYSTEM_OPERATINGSYSTEM_ISOSPLATFORM
using System.Runtime.InteropServices;
#endif
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -50,6 +53,24 @@ public sealed class LibUsbDotNetUsbHidEndPoint : IUsbHidEndPoint<UsbEndpointRead
private readonly int writeEndPointTimeoutInMilliseconds;
private readonly int readEndPointTimeoutInMilliseconds;

/// <summary>
/// Gets or sets the value indicating whether the actual read length
/// includes the report ID length (1 byte).
/// </summary>
/// <value>
/// <see langword="true"/> if the report ID is determined to be included;
/// <see langword="false"/> if it is determined not to be included;
/// <see langword="null"/> if it cannot be determined either way.
/// </value>
/// <remarks>
/// On Windows, the actual length read and set in the `transferred` pointer
/// parameter of `libusb_bulk_transfer` and `libusb_interrupt_transfer`
/// may include the length of the report ID. If a read operation on an
/// endpoint determines that the read length includes the report ID,
/// this value is set to `true`.
/// </remarks>
private bool? AssumesActualReadLengthIncludesReportId { get; set; }

internal LibUsbDotNetUsbHidEndPoint(
LibUsbDotNetUsbHidDevice device,
UsbEndpointWriter? endPointWriter,
Expand All @@ -69,6 +90,19 @@ bool shouldDisposeDevice
this.maxOutEndPointPacketSize = maxOutEndPointPacketSize;
writeEndPointTimeoutInMilliseconds = (int)writeEndPointTimeout.TotalMilliseconds;
this.shouldDisposeDevice = shouldDisposeDevice;

// As of now, on platforms other than Windows, it is considered that the
// read length does not include the report ID if the ID is 0.
if (!IsRunningOnWindows())
AssumesActualReadLengthIncludesReportId = false;

static bool IsRunningOnWindows()
=>
#if SYSTEM_OPERATINGSYSTEM_ISOSPLATFORM
OperatingSystem.IsWindows();
#else
RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif
}

private void ThrowIfDisposed()
Expand Down Expand Up @@ -118,20 +152,17 @@ public ValueTask DisposeAsync()
#endif

/// <inheritdoc/>
/// <remarks>
/// The first byte of the <paramref name="buffer"/>, which is for the Report ID, is ignored
/// because the underlying LibUsbDotNet API does not require it.
/// </remarks>
public void Write(ReadOnlySpan<byte> buffer, CancellationToken cancellationToken)
{
if (buffer.IsEmpty)
return;

buffer = buffer.Slice(LengthOfReportId); // get the slice of the payload only, excluding the report ID

if (maxOutEndPointPacketSize < buffer.Length)
if (LengthOfReportId + maxOutEndPointPacketSize < buffer.Length)
throw new ArgumentException($"length of the buffer must be less than or equals to maximum output packet length ({maxOutEndPointPacketSize})", nameof(buffer));

if (buffer[0] == 0x00) // If the report ID is 0, send only the payload
buffer = buffer.Slice(LengthOfReportId);

ThrowIfDisposed();

if (WriteEndPoint is null)
Expand Down Expand Up @@ -173,14 +204,8 @@ int timeout

/// <inheritdoc/>
/// <remarks>
/// <para>
/// The first byte of the <paramref name="buffer"/>, which is for the Report ID, is ignored
/// because the underlying LibUsbDotNet API does not require it.
/// </para>
/// <para>
/// This implementation performs a synchronous write, as the underlying
/// <see cref="UsbEndpointWriter"/> does not support asynchronous operations.
/// </para>
/// </remarks>
public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cancellationToken)
{
Expand All @@ -192,16 +217,14 @@ public ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, CancellationToken cance
/// <inheritdoc/>
/// <remarks>
/// The first byte of the <paramref name="buffer"/>, which is for the Report ID, is ignored
/// because the underlying LibUsbDotNet API does not require it.
/// in the current implementation.
/// </remarks>
public int Read(Span<byte> buffer, CancellationToken cancellationToken)
{
if (buffer.IsEmpty)
return 0;

buffer = buffer.Slice(LengthOfReportId); // get the slice of the payload only, excluding the report ID

if (maxInEndPointPacketSize < buffer.Length)
if (LengthOfReportId + maxInEndPointPacketSize < buffer.Length)
throw new ArgumentException($"length of the buffer must be less than or equals to maximum input packet length ({maxInEndPointPacketSize})", nameof(buffer));

ThrowIfDisposed();
Expand All @@ -212,12 +235,14 @@ public int Read(Span<byte> buffer, CancellationToken cancellationToken)
cancellationToken.ThrowIfCancellationRequested();

return ReadCore(
ReadEndPoint,
buffer,
readEndPointTimeoutInMilliseconds
thisEndPoint: this,
reader: ReadEndPoint,
buffer: buffer,
timeout: readEndPointTimeoutInMilliseconds
);

static int ReadCore(
LibUsbDotNetUsbHidEndPoint thisEndPoint,
UsbEndpointReader reader,
Span<byte> buffer,
int timeout
Expand All @@ -226,17 +251,23 @@ int timeout
var rentBuffer = ArrayPool<byte>.Shared.Rent(buffer.Length);

try {
// TODO: The current implementation assumes that the report ID is 0
// and that the read data does not include the report ID.
_ = reader.Read(
buffer: rentBuffer,
offset: 0,
count: buffer.Length,
offset: LengthOfReportId,
count: buffer.Length - LengthOfReportId,
timeout: timeout,
out var transferLength
);

rentBuffer.AsSpan(0, transferLength).CopyTo(buffer);

return transferLength + LengthOfReportId;
return CalculateActualReadLength(
endPoint: thisEndPoint,
requestedReadLengthExcludeReportId: buffer.Length,
actualReadLength: transferLength
);
}
finally {
if (rentBuffer is not null)
Expand All @@ -249,7 +280,7 @@ out var transferLength
/// <remarks>
/// <para>
/// The first byte of the <paramref name="buffer"/>, which is for the Report ID, is ignored
/// because the underlying LibUsbDotNet API does not require it.
/// in the current implementation.
/// </para>
/// <para>
/// This implementation performs a synchronous write, as the underlying
Expand All @@ -267,6 +298,27 @@ public ValueTask<int> ReadAsync(Memory<byte> buffer, CancellationToken cancellat
result: Read(buffer.Span, cancellationToken)
);

private static int CalculateActualReadLength(
LibUsbDotNetUsbHidEndPoint endPoint,
int requestedReadLengthExcludeReportId,
int actualReadLength
)
{
if (!endPoint.AssumesActualReadLengthIncludesReportId.HasValue) {
// If the actual read length is equal to '(Report ID + requested read
// data length)', the report ID is considered to be included in the
// actual read length (e.g., if a request was made to read a 64-byte
// payload and the actual read length turned out to be 65 bytes).
if (LengthOfReportId + requestedReadLengthExcludeReportId == actualReadLength)
endPoint.AssumesActualReadLengthIncludesReportId = true;
}

if (endPoint.AssumesActualReadLengthIncludesReportId ?? false)
return actualReadLength;
else
return LengthOfReportId + actualReadLength;
}

public override string? ToString()
=> $"{GetType().FullName} (Device='{device}', ReadEndPoint='{ReadEndPoint}', WriteEndPoint='{WriteEndPoint}')";
}
Loading
Loading