ScpControl/RootHub.cs (436 lines of code) (raw):

using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Reflection; using System.ServiceModel; using Libarius.System; using ReactiveSockets; using ScpControl.Bluetooth; using ScpControl.Driver; using ScpControl.Exceptions; using ScpControl.Profiler; using ScpControl.Properties; using ScpControl.Rx; using ScpControl.ScpCore; using ScpControl.Shared.Core; using ScpControl.Sound; using ScpControl.Usb; using ScpControl.Usb.Ds3; using ScpControl.Usb.Ds4; using ScpControl.Usb.Gamepads; using ScpControl.Utilities; using ScpControl.Wcf; namespace ScpControl { [ServiceBehavior(IncludeExceptionDetailInFaults = false, InstanceContextMode = InstanceContextMode.Single)] public sealed partial class RootHub : ScpHub, IScpCommandService { #region Private fields // Bluetooth hub private readonly BthHub _bthHub = new BthHub(); private readonly byte[][] _mNative = { new byte[2] {0, 0}, new byte[2] {0, 0}, new byte[2] {0, 0}, new byte[2] {0, 0} }; private readonly PhysicalAddress[] _reservedPads = { PhysicalAddress.None, PhysicalAddress.None, PhysicalAddress.None, PhysicalAddress.None }; private readonly byte[][] _vibration = { new byte[2] {0, 0}, new byte[2] {0, 0}, new byte[2] {0, 0}, new byte[2] {0, 0} }; // subscribed clients who receive the native stream private readonly IDictionary<int, ScpNativeFeedChannel> _nativeFeedSubscribers = new ConcurrentDictionary<int, ScpNativeFeedChannel>(); // virtual bus wrapper private readonly BusDevice _scpBus = new BusDevice(); // Usb hub private readonly UsbHub _usbHub = new UsbHub(); // creates a system-wide mutex to check if the root hub has been instantiated already private LimitInstance _limitInstance; private volatile bool _mSuspended; // the WCF service host private ServiceHost _rootHubServiceHost; // server to broadcast native byte stream private ReactiveListener _rxFeedServer; private bool _serviceStarted; #endregion #region IScpCommandService methods /// <summary> /// Checks if the native stream is available or disabled in configuration. /// </summary> /// <returns>True if feed is available, false otherwise.</returns> public bool IsNativeFeedAvailable() { return !GlobalConfiguration.Instance.DisableNative; } public DualShockPadMeta GetPadDetail(DsPadId pad) { var serial = (byte)pad; lock (Pads) { var current = Pads[serial]; return new DualShockPadMeta { BatteryStatus = (byte)current.Battery, ConnectionType = current.Connection, Model = current.Model, PadId = current.PadId, PadMacAddress = current.DeviceAddress, PadState = current.State }; } } public bool Rumble(DsPadId pad, byte large, byte small) { var serial = (byte)pad; if (Pads[serial].State != DsState.Connected) return false; if (large == _mNative[serial][0] && small == _mNative[serial][1]) return false; _mNative[serial][0] = large; _mNative[serial][1] = small; Pads[serial].Rumble(large, small); return true; } public IEnumerable<string> GetStatusData() { if (!_serviceStarted) return default(IEnumerable<string>); var list = new List<string> { Dongle, Pads[0].ToString(), Pads[1].ToString(), Pads[2].ToString(), Pads[3].ToString() }; return list; } public void PromotePad(byte pad) { int target = pad; if (Pads[target].State == DsState.Disconnected) return; var swap = Pads[target]; Pads[target] = Pads[target - 1]; Pads[target - 1] = swap; Pads[target].PadId = (DsPadId)target; Pads[target - 1].PadId = (DsPadId)(target - 1); _reservedPads[target] = Pads[target].DeviceAddress; _reservedPads[target - 1] = Pads[target - 1].DeviceAddress; } /// <summary> /// Requests the currently active configuration set from the root hub. /// </summary> /// <returns>Returns the global configuration object.</returns> public GlobalConfiguration RequestConfiguration() { return GlobalConfiguration.Request(); } /// <summary> /// Submits an altered copy of the global configuration to the root hub and saves it. /// </summary> /// <param name="configuration">The global configuration object.</param> public void SubmitConfiguration(GlobalConfiguration configuration) { GlobalConfiguration.Submit(configuration); GlobalConfiguration.Save(); } public IEnumerable<DualShockProfile> GetProfiles() { return DualShockProfileManager.Instance.Profiles; } public void SubmitProfile(DualShockProfile profile) { DualShockProfileManager.Instance.SubmitProfile(profile); } public void RemoveProfile(DualShockProfile profile) { DualShockProfileManager.Instance.RemoveProfile(profile); } #endregion #region Ctors public RootHub() { InitializeComponent(); // prepare "empty" pad list Pads = new List<IDsDevice> { new DsNull(DsPadId.One), new DsNull(DsPadId.Two), new DsNull(DsPadId.Three), new DsNull(DsPadId.Four) }; // subscribe to device plug-in events _bthHub.Arrival += OnDeviceArrival; _usbHub.Arrival += OnDeviceArrival; // subscribe to incoming HID reports _bthHub.Report += OnHidReportReceived; _usbHub.Report += OnHidReportReceived; } public RootHub(IContainer container) : this() { container.Add(this); } #endregion #region Properties /// <summary> /// A collection of currently connected game pads. /// </summary> public IList<IDsDevice> Pads { get; private set; } [Obsolete] public string Dongle { get { return _bthHub.Dongle; } } /// <summary> /// The MAC address of the current Bluetooth host. /// </summary> public PhysicalAddress BluetoothHostAddress { get { return _bthHub.BluetoothHostAddress; } } public bool Pairable { get { return m_Started && _bthHub.Pairable; } } #endregion #region Actions /// <summary> /// Opens and initializes devices and services listening and running on the local machine. /// </summary> /// <returns>True on success, false otherwise.</returns> public override bool Open() { var opened = false; Log.Debug("Initializing root hub"); _limitInstance = new LimitInstance(@"Global\ScpDsxRootHub"); try { if (!_limitInstance.IsOnlyInstance) // existing root hub running as desktop app throw new RootHubAlreadyStartedException( "The root hub is already running, please close the ScpServer first!"); } catch (UnauthorizedAccessException) // existing root hub running as service { throw new RootHubAlreadyStartedException( "The root hub is already running, please stop the ScpService first!"); } Log.DebugFormat("++ {0} {1}", Assembly.GetExecutingAssembly().Location, Assembly.GetExecutingAssembly().GetName().Version); Log.DebugFormat("++ {0}", OsInfoHelper.OsInfo); #region Native feed server _rxFeedServer = new ReactiveListener(Settings.Default.RootHubNativeFeedPort); _rxFeedServer.Connections.Subscribe(socket => { Log.DebugFormat("Client connected on native feed channel: {0}", socket.GetHashCode()); var protocol = new ScpNativeFeedChannel(socket); _nativeFeedSubscribers.Add(socket.GetHashCode(), protocol); protocol.Receiver.Subscribe(packet => { Log.Warn("Uuuhh how did we end up here?!"); }); socket.Disconnected += (sender, e) => { Log.DebugFormat( "Client disconnected from native feed channel {0}", sender.GetHashCode()); _nativeFeedSubscribers.Remove(socket.GetHashCode()); }; socket.Disposed += (sender, e) => { Log.DebugFormat("Client disposed from native feed channel {0}", sender.GetHashCode()); _nativeFeedSubscribers.Remove(socket.GetHashCode()); }; }); #endregion opened |= _scpBus.Open(GlobalConfiguration.Instance.Bus); opened |= _usbHub.Open(); opened |= _bthHub.Open(); GlobalConfiguration.Load(); return opened; } /// <summary> /// Starts listening for incoming requests and starts all underlying hubs. /// </summary> /// <returns>True on success, false otherwise.</returns> public override bool Start() { if (m_Started) return m_Started; Log.Debug("Starting root hub"); if (!_serviceStarted) { var baseAddress = new Uri("net.tcp://localhost:26760/ScpRootHubService"); var binding = new NetTcpBinding { TransferMode = TransferMode.Streamed, Security = new NetTcpSecurity { Mode = SecurityMode.None } }; _rootHubServiceHost = new ServiceHost(this, baseAddress); _rootHubServiceHost.AddServiceEndpoint(typeof(IScpCommandService), binding, baseAddress); _rootHubServiceHost.Open(); _serviceStarted = true; } try { _rxFeedServer.Start(); } catch (SocketException sex) { Log.FatalFormat("Couldn't start native feed server: {0}", sex); return false; } m_Started |= _scpBus.Start(); m_Started |= _usbHub.Start(); m_Started |= _bthHub.Start(); Log.Debug("Root hub started"); // make some noise =) if (GlobalConfiguration.Instance.IsStartupSoundEnabled) AudioPlayer.Instance.PlayCustomFile(GlobalConfiguration.Instance.StartupSoundFile); return m_Started; } /// <summary> /// Stops all underlying hubs and disposes acquired resources. /// </summary> /// <returns>True on success, false otherwise.</returns> public override bool Stop() { Log.Debug("Root hub stop requested"); _serviceStarted = false; if (_rootHubServiceHost != null) _rootHubServiceHost.Close(); if (_rxFeedServer != null) _rxFeedServer.Dispose(); _scpBus.Stop(); _usbHub.Stop(); _bthHub.Stop(); m_Started = !m_Started; Log.Debug("Root hub stopped"); _limitInstance.Dispose(); return true; } /// <summary> /// Stops all underlying hubs, disposes acquired resources and saves the global configuration. /// </summary> /// <returns>True on success, false otherwise.</returns> public override bool Close() { var retval = Stop(); GlobalConfiguration.Save(); return retval; } public override bool Suspend() { _mSuspended = true; lock (Pads) { foreach (var t in Pads) t.Disconnect(); } _scpBus.Suspend(); _usbHub.Suspend(); _bthHub.Suspend(); Log.Debug("++ Suspended"); return true; } public override bool Resume() { Log.Debug("++ Resumed"); _scpBus.Resume(); for (var index = 0; index < Pads.Count; index++) { if (Pads[index].State != DsState.Disconnected) { _scpBus.Plugin(index + 1); } } _usbHub.Resume(); _bthHub.Resume(); _mSuspended = false; return true; } #endregion #region Events /// <summary> /// Gets called when a device was plugged in. /// </summary> /// <param name="notification">The <see cref="ScpDevice.Notified"/> type.</param> /// <param name="Class">The device class of the currently affected device.</param> /// <param name="path">The device path of the currently affected device.</param> /// <returns></returns> public override DsPadId Notify(ScpDevice.Notified notification, string Class, string path) { // ignore while component is in sleep mode if (_mSuspended) return DsPadId.None; var classGuid = Guid.Parse(Class); // forward message for wired DS4 to usb hub if (classGuid == UsbDs4.DeviceClassGuid) { return _usbHub.Notify(notification, Class, path); } // forward message for wired DS3 to usb hub if (classGuid == UsbDs3.DeviceClassGuid) { return _usbHub.Notify(notification, Class, path); } // forward message for wired Generic Gamepad to usb hub if (classGuid == UsbGenericGamepad.DeviceClassGuid) { return _usbHub.Notify(notification, Class, path); } // forward message for any wireless device to bluetooth hub if (classGuid == BthDongle.DeviceClassGuid) { _bthHub.Notify(notification, Class, path); } return DsPadId.None; } protected override void OnDeviceArrival(object sender, ArrivalEventArgs e) { var bFound = false; var arrived = e.Device; lock (Pads) { for (var index = 0; index < Pads.Count && !bFound; index++) { if (arrived.DeviceAddress.Equals(_reservedPads[index])) { if (Pads[index].State == DsState.Connected) { if (Pads[index].Connection == DsConnection.Bluetooth) { Pads[index].Disconnect(); } if (Pads[index].Connection == DsConnection.Usb) { arrived.Disconnect(); e.Handled = false; return; } } bFound = true; arrived.PadId = (DsPadId)index; Pads[index] = arrived; } } for (var index = 0; index < Pads.Count && !bFound; index++) { if (Pads[index].State == DsState.Disconnected) { bFound = true; _reservedPads[index] = arrived.DeviceAddress; arrived.PadId = (DsPadId)index; Pads[index] = arrived; } } } if (bFound) { _scpBus.Plugin((int)arrived.PadId + 1); if (!GlobalConfiguration.Instance.IsVBusDisabled) { Log.InfoFormat("Plugged in Port #{0} for {1} on Virtual Bus", (int)arrived.PadId + 1, arrived.DeviceAddress.AsFriendlyName()); } } e.Handled = bFound; } protected override void OnHidReportReceived(object sender, ScpHidReport e) { // get current pad ID var serial = (int)e.PadId; if (GlobalConfiguration.Instance.ProfilesEnabled) { // pass current report through user profiles DualShockProfileManager.Instance.PassThroughAllProfiles(e); } if (e.PadState == DsState.Connected) { // translate current report to Xbox format and send it to bus device XOutputWrapper.Instance.SetState((uint) serial, _scpBus.Parse(e)); // set currently assigned XInput slot Pads[serial].XInputSlot = XOutputWrapper.Instance.GetRealIndex((uint) serial); byte largeMotor = 0; byte smallMotor = 0; // forward rumble request to pad if (XOutputWrapper.Instance.GetState((uint) serial, ref largeMotor, ref smallMotor) && (largeMotor != _vibration[serial][0] || smallMotor != _vibration[serial][1])) { _vibration[serial][0] = largeMotor; _vibration[serial][1] = smallMotor; Pads[serial].Rumble(largeMotor, smallMotor); } } else { // reset rumble/vibration to off state _vibration[serial][0] = _vibration[serial][1] = 0; _mNative[serial][0] = _mNative[serial][1] = 0; if (GlobalConfiguration.Instance.AlwaysUnPlugVirtualBusDevice) { _scpBus.Unplug(_scpBus.IndexToSerial((byte)e.PadId)); } } // skip broadcast if native feed is disabled if (GlobalConfiguration.Instance.DisableNative) return; // send native controller inputs to subscribed clients foreach ( var channel in _nativeFeedSubscribers.Select(nativeFeedSubscriber => nativeFeedSubscriber.Value)) { try { channel.SendAsync(e.RawBytes); } catch (AggregateException) { /* This might happen if the client disconnects while sending the * response is still in progress. The exception can be ignored. */ } } } #endregion } }