ScpControl/Usb/UsbDevice.cs (207 lines of code) (raw):
using System;
using System.Net.NetworkInformation;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using HidSharp.ReportDescriptors.Parser;
using ScpControl.ScpCore;
using ScpControl.Shared.Core;
using ScpControl.Sound;
using ScpControl.Utilities;
namespace ScpControl.Usb
{
/// <summary>
/// Represents a generic Usb device.
/// </summary>
public class UsbDevice : ScpDevice, IDsDevice
{
#region Private fields
private readonly IObservable<long> _outputReportSchedule = Observable.Interval(TimeSpan.FromMilliseconds(10),
Scheduler.Default);
private CancellationTokenSource _hidCancellationTokenSource = new CancellationTokenSource();
private IDisposable _outputReportTask;
private readonly TaskQueue _inputReportQueue = new TaskQueue();
#endregion
#region Private methods
/// <summary>
/// Worker thread polling for incoming Usb interrupts.
/// </summary>
/// <param name="o">Task cancellation token.</param>
private void HidWorker(object o)
{
var token = (CancellationToken) o;
var transfered = 0;
var buffer = new byte[64];
Log.Debug("-- Usb Device : HID_Worker_Thread Starting");
while (!token.IsCancellationRequested)
{
try
{
if (ReadIntPipe(buffer, buffer.Length, ref transfered) && transfered > 0)
{
ParseHidReport(buffer);
}
}
catch (Exception ex)
{
Log.ErrorFormat("Unexpected error: {0}", ex);
}
}
Log.Debug("-- Usb Device : HID_Worker_Thread Exiting");
}
#endregion
#region Protected fields
protected byte[] m_Buffer = new byte[64];
protected byte m_CableStatus = 0;
protected string m_Instance = string.Empty;
protected DateTime m_Last = DateTime.Now, m_Tick = DateTime.Now, m_Disconnect = DateTime.Now;
protected uint PacketCounter;
protected byte m_PlugStatus = 0;
protected bool m_Publish = false;
protected readonly ReportDescriptorParser ReportDescriptor = new ReportDescriptorParser();
#endregion
#region Events
public event EventHandler<ScpHidReport> HidReportReceived;
protected void OnHidReportReceived(ScpHidReport report)
{
if (GlobalConfiguration.Instance.UseAsyncHidReportProcessing)
{
_inputReportQueue.Enqueue(() => Task.Run(() =>
{
if (HidReportReceived != null)
HidReportReceived.Invoke(this, report);
}));
}
else
{
if (HidReportReceived != null)
HidReportReceived.Invoke(this, report);
}
}
#endregion
#region Ctors
protected UsbDevice(Guid guid)
: base(guid)
{
}
public UsbDevice()
{
}
#endregion
#region Properties
public virtual bool IsShutdown { get; set; }
/// <summary>
/// Controller model.
/// </summary>
public virtual DsModel Model { get; protected set; }
public virtual DsPadId PadId { get; set; }
public uint? XInputSlot { get; set; }
/// <summary>
/// Controller connection type.
/// </summary>
public virtual DsConnection Connection
{
get { return DsConnection.Usb; }
}
/// <summary>
/// Controller connection state.
/// </summary>
public virtual DsState State { get; protected set; }
/// <summary>
/// Battery charging level.
/// </summary>
public virtual DsBattery Battery { get; protected set; }
public virtual PhysicalAddress DeviceAddress { get; protected set; }
public virtual PhysicalAddress HostAddress { get; protected set; }
#endregion
#region Public methods
/// <summary>
/// Crafts a new <see cref="ScpHidReport"/> with current devices meta data.
/// </summary>
/// <returns>The new HID <see cref="ScpHidReport"/>.</returns>
public ScpHidReport NewHidReport()
{
return new ScpHidReport
{
PadId = PadId,
PadState = State,
ConnectionType = Connection,
Model = Model,
PadMacAddress = DeviceAddress,
BatteryStatus = (byte) Battery
};
}
public override bool Start()
{
if (!IsActive) return State == DsState.Connected;
State = DsState.Connected;
PacketCounter = 0;
Task.Factory.StartNew(HidWorker, _hidCancellationTokenSource.Token);
_outputReportTask = _outputReportSchedule.Subscribe(tick => Process(DateTime.Now));
Rumble(0, 0);
Log.DebugFormat("-- Started Device Instance [{0}] Local [{1}] Remote [{2}]", m_Instance,
DeviceAddress.AsFriendlyName(), HostAddress.AsFriendlyName());
// connection sound
if (GlobalConfiguration.Instance.IsUsbConnectSoundEnabled)
AudioPlayer.Instance.PlayCustomFile(GlobalConfiguration.Instance.UsbConnectSoundFile);
#region Request HID Report Descriptor
// try to retrieve HID Report Descriptor
var buffer = new byte[512];
var transfered = 0;
if (SendTransfer(UsbHidRequestType.GetDescriptor, UsbHidRequest.GetDescriptor,
ToValue(UsbHidClassDescriptorType.Report),
buffer, ref transfered) && transfered > 0)
{
Log.DebugFormat("-- HID Report Descriptor: {0}", buffer.ToHexString(transfered));
// store report descriptor
ReportDescriptor.Parse(buffer);
}
#endregion
return State == DsState.Connected;
}
public virtual bool Rumble(byte large, byte small)
{
return false;
}
public virtual bool Pair(PhysicalAddress master)
{
return false;
}
public virtual bool Disconnect()
{
return true;
}
public override bool Stop()
{
if (IsActive)
{
if (_outputReportTask != null)
_outputReportTask.Dispose();
State = GlobalConfiguration.Instance.ReservePadSlot ? DsState.Reserved : DsState.Disconnected;
_hidCancellationTokenSource.Cancel();
_hidCancellationTokenSource = new CancellationTokenSource();
OnHidReportReceived(NewHidReport());
}
return base.Stop();
}
public override bool Close()
{
if (IsActive)
{
base.Close();
if (_outputReportTask != null)
_outputReportTask.Dispose();
State = DsState.Disconnected;
OnHidReportReceived(NewHidReport());
}
return !IsActive;
}
public override string ToString()
{
switch (State)
{
case DsState.Disconnected:
return string.Format("Pad {0} : Disconnected", PadId);
case DsState.Reserved:
return string.Format("Pad {0} : {1} {2} - Reserved", PadId, Model, DeviceAddress.AsFriendlyName());
case DsState.Connected:
return string.Format("Pad {0} : {1} {2} - {3} {4:X8} {5}", PadId, Model,
DeviceAddress.AsFriendlyName(),
Connection,
PacketCounter,
Battery
);
}
throw new Exception();
}
#endregion
#region Protected methods
protected virtual void Process(DateTime now)
{
}
protected virtual void ParseHidReport(byte[] report)
{
}
protected virtual bool Shutdown()
{
Stop();
return RestartDevice(m_Instance);
}
#endregion
}
}