using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; #region FFXIV_Vibe_Plugin deps using FFXIV_Vibe_Plugin.Commons; #endregion #region Other deps using Buttplug; #endregion namespace FFXIV_Vibe_Plugin.Device { public class DevicesController { private readonly Logger Logger; private readonly Configuration Configuration; private ConfigurationProfile Profile; private readonly Patterns Patterns; private Triggers.Trigger? CurrentPlayingTrigger; /** * State of the current device and motor when it started to play as a unix timestamp. * This is used to detect if a thread that runs a pattern should stop */ private readonly Dictionary CurrentDeviceAndMotorPlaying = new(); // Buttplug related private ButtplugClient? BPClient; private readonly List Devices = new(); private bool isScanning = false; // Internal variables private readonly static Mutex mut = new(); public DevicesController(Logger logger, Configuration configuration, ConfigurationProfile profile, Patterns patterns) { this.Logger = logger; this.Configuration = configuration; this.Profile = profile; this.Patterns = patterns; } public void Dispose() { this.Disconnect(); } public void SetProfile(ConfigurationProfile profile) { this.Profile = profile; } public void Connect(String host, int port) { if(this.IsConnected()) { this.Logger.Debug("Disconnecting previous instance! Waiting 2sec..."); this.Disconnect(); Thread.Sleep(200); } try { this.BPClient = new("bp-dalamud"); } catch(Exception e) { this.Logger.Error($"Can't load bp.", e); return; } this.BPClient.ServerDisconnect += BPClient_ServerDisconnected; this.BPClient.DeviceAdded += BPClient_DeviceAdded; this.BPClient.DeviceRemoved += BPClient_DeviceRemoved; this.BPClient.ScanningFinished += BPClient_OnScanComplete; string hostandport = host + ":" + port.ToString(); try { var uri = new Uri($"ws://{hostandport}/buttplug"); var connector = new ButtplugWebsocketConnectorOptions(uri); this.Logger.Log($"Connecting to {hostandport}."); Task task = this.BPClient.ConnectAsync(connector); task.Wait(); this.ScanDevice(); } catch(Exception e) { this.Logger.Error($"Could not connect to {hostandport}.", e); } Thread.Sleep(200); if(this.BPClient.Connected) { this.Logger.Log($"FVP connected to Intiface!"); } else { this.Logger.Error("Failed connecting (Intiface server is up?)"); return; } } private void BPClient_ServerDisconnected(object? sender, EventArgs e) { this.Logger.Debug("Server disconnected"); this.Disconnect(); } public bool IsConnected() { bool isConnected = false; if(this.BPClient != null) { isConnected = this.BPClient.Connected; } return isConnected; } public void ScanDevice() { if(this.BPClient == null) { return; } this.Logger.Debug("Scanning for devices..."); if(this.IsConnected()) { try { this.isScanning = true; var task = this.BPClient.StartScanningAsync(); task.Wait(); } catch(Exception e) { this.isScanning = false; this.Logger.Error("Scanning issue. No 'Device Comm Managers' enabled on Intiface?"); this.Logger.Error(e.Message); } } } public bool IsScanning() { return this.isScanning; } public void StopScanningDevice() { if(this.BPClient != null && this.IsConnected()) { try { Task task = this.BPClient.StopScanningAsync(); task.Wait(); } catch(Exception) { this.Logger.Debug("StopScanningDevice ignored: already stopped"); } } this.isScanning = false; } private void BPClient_OnScanComplete(object? sender, EventArgs e) { this.Logger.Debug("Stop scanning..."); // FIXME: this is not working, bp client emit the trigger instantly. Let's ignore for the moment. // this.isScanning = false; } private void BPClient_DeviceAdded(object? sender, DeviceAddedEventArgs arg) { try { mut.WaitOne(); ButtplugClientDevice BPClientDevice = arg.Device; Device device = new(BPClientDevice); device.IsConnected = true; this.Logger.Log($"{arg.Device.Name}, {BPClientDevice.Name}"); this.Devices.Add(device); if(!this.Profile.VISITED_DEVICES.ContainsKey(device.Name)) { this.Profile.VISITED_DEVICES[device.Name] = device; this.Configuration.Save(); this.Logger.Debug($"Adding device to visited list {device})"); } this.Logger.Debug($"Added {device})"); } finally { mut.ReleaseMutex(); } } private void BPClient_DeviceRemoved(object? sender, DeviceRemovedEventArgs e) { try { mut.WaitOne(); int index = this.Devices.FindIndex(device => device.Id == e.Device.Index); if(index > -1) { this.Logger.Debug($"Removed {Devices[index]}"); Device device = Devices[index]; this.Devices.RemoveAt(index); device.IsConnected = false; } } finally { mut.ReleaseMutex(); } } public void Disconnect() { this.Devices.Clear(); if(this.BPClient == null || !this.IsConnected()) { return; } try { if(this.BPClient.IsScanning) { var task = this.BPClient.StopScanningAsync(); task.Wait(); } } catch(Exception e) { this.Logger.Error("Couldn't stop scanning device... Unknown reason."); this.Logger.Error(e.Message); } try { for(int i = 0; i < this.BPClient.Devices.Length; i++) { this.Logger.Log($"Disconnecting device {i} {this.BPClient.Devices[i].Name}"); this.BPClient.Devices[i].Dispose(); } } catch(Exception e) { this.Logger.Error("Error while disconnecting device", e); } try { Thread.Sleep(1000); if(this.BPClient != null) { this.BPClient.DisconnectAsync(); this.Logger.Log("Disconnecting! Bye... Waiting 2sec..."); } } catch(Exception e) { // ignore exception, we are trying to do our best this.Logger.Error("Error while disconnecting client", e); } this.BPClient = null; } public List GetDevices() { return this.Devices; } public Dictionary GetVisitedDevices() { return this.Profile.VISITED_DEVICES; } public void UpdateAllBatteryLevel() { foreach(Device device in this.GetDevices()) { device.UpdateBatteryLevel(); } } public void StopAll() { foreach(Device device in this.GetDevices()) { device.Stop(); } } public void SendTrigger(Triggers.Trigger trigger, int threshold=100) { if(!this.IsConnected()) { this.Logger.Debug($"Not connected, cannot send ${trigger}"); return; } this.Logger.Debug($"Sending trigger {trigger} (priority={trigger.Priority})"); // Check if the trigger has the priority if(this.CurrentPlayingTrigger == null) { this.CurrentPlayingTrigger = trigger; } if(trigger.Priority < this.CurrentPlayingTrigger.Priority) { this.Logger.Debug($"Ignoring trigger because lower priority => {trigger} < {this.CurrentPlayingTrigger}"); return; } this.CurrentPlayingTrigger = trigger; foreach(Triggers.TriggerDevice triggerDevice in trigger.Devices) { Device? device = this.FindDevice(triggerDevice.Name); if(device != null && triggerDevice != null) { if(triggerDevice.ShouldVibrate) { for(int motorId = 0; motorId < triggerDevice.VibrateSelectedMotors?.Length; motorId++) { if(triggerDevice.VibrateSelectedMotors != null && triggerDevice.VibrateMotorsThreshold != null) { bool motorEnabled = triggerDevice.VibrateSelectedMotors[motorId]; int motorThreshold = triggerDevice.VibrateMotorsThreshold[motorId] * threshold / 100; int motorPatternId = triggerDevice.VibrateMotorsPattern[motorId]; float startAfter = trigger.StartAfter; float stopAfter = trigger.StopAfter; if(motorEnabled) { this.Logger.Debug($"Sending {device.Name} vibration to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!"); this.SendPattern("vibrate", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter); } } } } if(triggerDevice.ShouldRotate) { for(int motorId = 0; motorId < triggerDevice.RotateSelectedMotors?.Length; motorId++) { if(triggerDevice.RotateSelectedMotors != null && triggerDevice.RotateMotorsThreshold != null) { bool motorEnabled = triggerDevice.RotateSelectedMotors[motorId]; int motorThreshold = triggerDevice.RotateMotorsThreshold[motorId] * threshold / 100; int motorPatternId = triggerDevice.RotateMotorsPattern[motorId]; float startAfter = trigger.StartAfter; float stopAfter = trigger.StopAfter; if(motorEnabled) { this.Logger.Debug($"Sending {device.Name} rotation to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!"); this.SendPattern("rotate", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter); } } } } if(triggerDevice.ShouldLinear) { for(int motorId = 0; motorId < triggerDevice.LinearSelectedMotors?.Length; motorId++) { if(triggerDevice.LinearSelectedMotors != null && triggerDevice.LinearMotorsThreshold != null) { bool motorEnabled = triggerDevice.LinearSelectedMotors[motorId]; int motorThreshold = triggerDevice.LinearMotorsThreshold[motorId] * threshold / 100; int motorPatternId = triggerDevice.LinearMotorsPattern[motorId]; float startAfter = trigger.StartAfter; float stopAfter = trigger.StopAfter; if(motorEnabled) { this.Logger.Debug($"Sending {device.Name} linear to motor: {motorId} patternId={motorPatternId} with threshold: {motorThreshold}!"); this.SendPattern("linear", device, motorThreshold, motorId, motorPatternId, startAfter, stopAfter); } } } } if(triggerDevice.ShouldStop) { this.Logger.Debug($"Sending stop to {device.Name}!"); DevicesController.SendStop(device); } } } } /** Search for a device with the corresponding text */ public Device? FindDevice(string text) { Device? foundDevice = null; foreach(Device device in this.Devices) { if(device.Name.Contains(text) && device != null) { foundDevice = device; } } return foundDevice; } /** * Sends an itensity vibe to all of the devices * @param {float} intensity */ public void SendVibeToAll(int intensity) { if(this.IsConnected() && this.BPClient != null) { foreach(Device device in this.Devices) { device.SendVibrate(intensity, -1, this.Profile.MAX_VIBE_THRESHOLD); device.SendRotate(intensity, true, -1, this.Profile.MAX_VIBE_THRESHOLD); device.SendLinear(intensity, 500, -1, this.Profile.MAX_VIBE_THRESHOLD); } } } public void SendPattern(string command, Device device, int threshold, int motorId = -1, int patternId = 0, float StartAfter = 0, float StopAfter = 0) { this.SaveCurrentMotorAndDevicePlayingState(device, motorId); Pattern pattern = Patterns.GetPatternById(patternId); string[] patternSegments = pattern.Value.Split("|"); this.Logger.Log($"SendPattern '{command}' pattern={pattern.Name} ({patternSegments.Length} segments) to {device} motor={motorId} startAfter={StartAfter} stopAfter={StopAfter} threshold={threshold}"); string deviceAndMotorId = $"{device.Name}:{motorId}"; int startedUnixTime = this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]; // Make sure things stops if StopAfter is set by sending a zero. // We make sure to send the zero to the correct device and if it is still running. bool forceStop = false; Thread tStopAfter = new(delegate () { if(StopAfter == 0) { return; } Thread.Sleep((int)StopAfter * 1000); if(startedUnixTime == this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) { forceStop = true; this.SendCommand(command, device, 0, motorId); this.Logger.Debug($"Force stopping {deviceAndMotorId} because of StopAfter={StopAfter}"); // Make sure we clean the current playing trigger whgenever we're force stopping a trigger this.CurrentPlayingTrigger = null; } }); tStopAfter.Start(); Thread t = new(delegate () { Thread.Sleep((int)StartAfter * 1000); // Stop exectution if a new pattern is sent to the same device and motor. if(startedUnixTime != this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) { return; } // Experimental send a fake command to activate connection this.SendCommand(command, device, 0, motorId); Thread.Sleep(50); // Yield if necessary for(int segIndex = 0; segIndex < patternSegments.Length; segIndex++) { // Stop exectution if a new pattern is send to the same device and motor. if(startedUnixTime != this.CurrentDeviceAndMotorPlaying[deviceAndMotorId]) { break; } string patternSegment = patternSegments[segIndex]; string[] patternValues = patternSegment.Split(":"); int intensity = Helpers.ClampIntensity(Int32.Parse(patternValues[0]), threshold); int duration = Int32.Parse(patternValues[1]); //this.Logger.Debug($"SENDING SEGMENT: intensity={intensity} duration={duration}"); // Stop after and send 0 intensity if(forceStop || (StopAfter > 0 && StopAfter * 1000 + startedUnixTime < Helpers.GetUnix())) { this.SendCommand(command, device, 0, motorId, duration); break; } // Send the command \o/ this.SendCommand(command, device, intensity, motorId, duration); Thread.Sleep(duration); } }); t.Start(); } public void SendCommand(string command, Device device, int intensity, int motorId, int duration=500) { if(command == "vibrate") { this.SendVibrate(device, intensity, motorId); } else if(command == "rotate") { this.SendRotate(device, intensity, motorId); } else if(command == "linear") { this.SendLinear(device, intensity, motorId, duration); } } public void SendVibrate(Device device, int intensity, int motorId = -1) { device.SendVibrate(intensity, motorId, this.Profile.MAX_VIBE_THRESHOLD); } public void SendRotate(Device device, int intensity, int motorId = -1, bool clockwise = true) { device.SendRotate(intensity, clockwise, motorId, this.Profile.MAX_VIBE_THRESHOLD); } public void SendLinear(Device device, int intensity, int motorId = -1, int duration = 500) { device.SendLinear(intensity, duration, motorId, this.Profile.MAX_VIBE_THRESHOLD); } public static void SendStop(Device device) { device.Stop(); } private void SaveCurrentMotorAndDevicePlayingState(Device device, int motorId) { string deviceAndMotorId = $"{device.Name}:{motorId}"; this.CurrentDeviceAndMotorPlaying[deviceAndMotorId] = Helpers.GetUnix(); } } }