using System; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.Linq; using System.Net.NetworkInformation; using System.Numerics; using System.Threading; using System.Windows.Forms; using BetterJoyForCemu.Controller; using Nefarius.ViGEm.Client.Targets.DualShock4; using Nefarius.ViGEm.Client.Targets.Xbox360; namespace BetterJoyForCemu { public class Joycon { public string path = String.Empty; public bool isPro = false; public bool isSnes = false; public bool is64 = false; bool isUSB = false; private Joycon _other = null; // 64 vars float maxX = 0.5f; float minX = -0.5f; float maxY = 0.5f; float minY = -0.5f; public Joycon other { get { return _other; } set { _other = value; // If the other Joycon is itself, the Joycon is sideways if (_other == null || _other == this) { // Set LED to current Pad ID SetLEDByPlayerNum(PadId); } else { // Set LED to current Joycon Pair int lowestPadId = Math.Min(_other.PadId, PadId); SetLEDByPlayerNum(lowestPadId); } } } public bool active_gyro = false; private long inactivity = Stopwatch.GetTimestamp(); public bool send = true; public enum DebugType : int { NONE, ALL, COMMS, THREADING, IMU, RUMBLE, SHAKE, }; public DebugType debug_type = (DebugType)int.Parse(ConfigurationManager.AppSettings["DebugType"]); //public DebugType debug_type = DebugType.NONE; //Keep this for manual debugging during development. public bool isLeft; public enum state_ : uint { NOT_ATTACHED, DROPPED, NO_JOYCONS, ATTACHED, INPUT_MODE_0x30, IMU_DATA_OK, }; public state_ state; public enum Button : int { DPAD_DOWN = 0, DPAD_RIGHT = 1, DPAD_LEFT = 2, DPAD_UP = 3, SL = 4, SR = 5, MINUS = 6, HOME = 7, PLUS = 8, CAPTURE = 9, STICK = 10, SHOULDER_1 = 11, SHOULDER_2 = 12, // For pro controller B = 13, A = 14, Y = 15, X = 16, STICK2 = 17, SHOULDER2_1 = 18, SHOULDER2_2 = 19, }; private bool[] buttons_down = new bool[20]; private bool[] buttons_up = new bool[20]; private bool[] buttons = new bool[20]; private bool[] down_ = new bool[20]; private long[] buttons_down_timestamp = new long[20]; private float[] stick = { 0, 0 }; private float[] stick2 = { 0, 0 }; private IntPtr handle; byte[] default_buf = { 0x0, 0x1, 0x40, 0x40, 0x0, 0x1, 0x40, 0x40 }; private byte[] stick_raw = { 0, 0, 0 }; private UInt16[] stick_cal = { 0, 0, 0, 0, 0, 0 }; private UInt16 deadzone; private UInt16[] stick_precal = { 0, 0 }; private byte[] stick2_raw = { 0, 0, 0 }; private UInt16[] stick2_cal = { 0, 0, 0, 0, 0, 0 }; private UInt16 deadzone2; private UInt16[] stick2_precal = { 0, 0 }; private bool stop_polling = true; private bool imu_enabled = false; private Int16[] acc_r = { 0, 0, 0 }; private Int16[] acc_neutral = { 0, 0, 0 }; private Int16[] acc_sensiti = { 0, 0, 0 }; private Vector3 acc_g; private Int16[] gyr_r = { 0, 0, 0 }; private Int16[] gyr_neutral = { 0, 0, 0 }; private Int16[] gyr_sensiti = { 0, 0, 0 }; private Vector3 gyr_g; private float[] cur_rotation; // Filtered IMU data private short[] acc_sen = new short[3]{ 16384, 16384, 16384 }; private short[] gyr_sen = new short[3]{ 18642, 18642, 18642 }; private Int16[] pro_hor_offset = { -710, 0, 0 }; private Int16[] left_hor_offset = { 0, 0, 0 }; private Int16[] right_hor_offset = { 0, 0, 0 }; private bool do_localize; private float filterweight; private const uint report_len = 49; private struct Rumble { public Queue queue; public void set_vals(float low_freq, float high_freq, float amplitude) { float[] rumbleQueue = new float[] { low_freq, high_freq, amplitude }; // Keep a queue of 15 items, discard oldest item if queue is full. if (queue.Count > 15) { queue.Dequeue(); } queue.Enqueue(rumbleQueue); } public Rumble(float[] rumble_info) { queue = new Queue(); queue.Enqueue(rumble_info); } private float clamp(float x, float min, float max) { if (x < min) return min; if (x > max) return max; return x; } private byte EncodeAmp(float amp) { byte en_amp; if (amp == 0) en_amp = 0; else if (amp < 0.117) en_amp = (byte)(((Math.Log(amp * 1000, 2) * 32) - 0x60) / (5 - Math.Pow(amp, 2)) - 1); else if (amp < 0.23) en_amp = (byte)(((Math.Log(amp * 1000, 2) * 32) - 0x60) - 0x5c); else en_amp = (byte)((((Math.Log(amp * 1000, 2) * 32) - 0x60) * 2) - 0xf6); return en_amp; } public byte[] GetData() { byte[] rumble_data = new byte[8]; float[] queued_data = queue.Dequeue(); if (queued_data[2] == 0.0f) { rumble_data[0] = 0x0; rumble_data[1] = 0x1; rumble_data[2] = 0x40; rumble_data[3] = 0x40; } else { queued_data[0] = clamp(queued_data[0], 40.875885f, 626.286133f); queued_data[1] = clamp(queued_data[1], 81.75177f, 1252.572266f); queued_data[2] = clamp(queued_data[2], 0.0f, 1.0f); UInt16 hf = (UInt16)((Math.Round(32f * Math.Log(queued_data[1] * 0.1f, 2)) - 0x60) * 4); byte lf = (byte)(Math.Round(32f * Math.Log(queued_data[0] * 0.1f, 2)) - 0x40); byte hf_amp = EncodeAmp(queued_data[2]); UInt16 lf_amp = (UInt16)(Math.Round((double)hf_amp) * .5); byte parity = (byte)(lf_amp % 2); if (parity > 0) { --lf_amp; } lf_amp = (UInt16)(lf_amp >> 1); lf_amp += 0x40; if (parity > 0) lf_amp |= 0x8000; hf_amp = (byte)(hf_amp - (hf_amp % 2)); // make even at all times to prevent weird hum rumble_data[0] = (byte)(hf & 0xff); rumble_data[1] = (byte)(((hf >> 8) & 0xff) + hf_amp); rumble_data[2] = (byte)(((lf_amp >> 8) & 0xff) + lf); rumble_data[3] = (byte)(lf_amp & 0xff); } for (int i = 0; i < 4; ++i) { rumble_data[4 + i] = rumble_data[i]; } return rumble_data; } } private Rumble rumble_obj; private byte global_count = 0; private string debug_str; // For UdpServer public int PadId = 0; public int battery = -1; public int model = 2; public int constate = 2; public int connection = 3; public PhysicalAddress PadMacAddress = new PhysicalAddress(new byte[] { 01, 02, 03, 04, 05, 06 }); public ulong Timestamp = 0; public int packetCounter = 0; public OutputControllerXbox360 out_xbox; public OutputControllerDualShock4 out_ds4; ushort ds4_ts = 0; ulong lag; int lowFreq = Int32.Parse(ConfigurationManager.AppSettings["LowFreqRumble"]); int highFreq = Int32.Parse(ConfigurationManager.AppSettings["HighFreqRumble"]); bool toRumble = Boolean.Parse(ConfigurationManager.AppSettings["EnableRumble"]); bool showAsXInput = Boolean.Parse(ConfigurationManager.AppSettings["ShowAsXInput"]); bool showAsDS4 = Boolean.Parse(ConfigurationManager.AppSettings["ShowAsDS4"]); public MainForm form; public byte LED { get; private set; } = 0x0; public void SetLEDByPlayerNum(int id) { if (id > 3) { // No support for any higher than 3 (4 Joycons/Controllers supported in the application normally) id = 3; } if (ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None).AppSettings.Settings["UseIncrementalLights"].Value.ToLower() == "true") { // Set all LEDs from 0 to the given id to lit int ledId = id; LED = 0x0; do { LED |= (byte)(0x1 << ledId); } while (--ledId >= 0); } else { LED = (byte)(0x1 << id); } SetPlayerLED(LED); } public string serial_number; bool thirdParty = false; private float[] activeData; static float AHRS_beta = float.Parse(ConfigurationManager.AppSettings["AHRS_beta"]); private MadgwickAHRS AHRS = new MadgwickAHRS(0.005f, AHRS_beta); // for getting filtered Euler angles of rotation; 5ms sampling rate public Joycon(IntPtr handle_, bool imu, bool localize, float alpha, bool left, string path, string serialNum, int id = 0, bool isPro = false, bool isSnes = false, bool is64 = false, bool thirdParty = false) { serial_number = serialNum; activeData = new float[6]; handle = handle_; imu_enabled = imu; do_localize = localize; rumble_obj = new Rumble(new float[] { lowFreq, highFreq, 0 }); for (int i = 0; i < buttons_down_timestamp.Length; i++) buttons_down_timestamp[i] = -1; filterweight = alpha; isLeft = left; PadId = id; LED = (byte)(0x1 << PadId); this.isPro = isPro || isSnes || is64; this.isSnes = isSnes; this.is64 = is64; isUSB = serialNum == "000000000001"; this.thirdParty = thirdParty; this.path = path; connection = isUSB ? 0x01 : 0x02; if (showAsXInput) { out_xbox = new OutputControllerXbox360(); if (toRumble) out_xbox.FeedbackReceived += ReceiveRumble; } if (showAsDS4) { out_ds4 = new OutputControllerDualShock4(); if (toRumble) out_ds4.FeedbackReceived += Ds4_FeedbackReceived; } } public void getActiveData() { this.activeData = form.activeCaliData(serial_number); } public void ReceiveRumble(Xbox360FeedbackReceivedEventArgs e) { DebugPrint("Rumble data Recived: XInput", DebugType.RUMBLE); SetRumble(lowFreq, highFreq, (float)Math.Max(e.LargeMotor, e.SmallMotor) / (float)255); if (other != null && other != this) other.SetRumble(lowFreq, highFreq, (float)Math.Max(e.LargeMotor, e.SmallMotor) / (float)255); } public void Ds4_FeedbackReceived(DualShock4FeedbackReceivedEventArgs e) { DebugPrint("Rumble data Recived: DS4", DebugType.RUMBLE); SetRumble(lowFreq, highFreq, (float)Math.Max(e.LargeMotor, e.SmallMotor) / (float)255); if (other != null && other != this) other.SetRumble(lowFreq, highFreq, (float)Math.Max(e.LargeMotor, e.SmallMotor) / (float)255); } public void DebugPrint(String s, DebugType d) { if (debug_type == DebugType.NONE) return; if (d == DebugType.ALL || d == debug_type || debug_type == DebugType.ALL) { form.AppendTextBox(s + "\r\n"); } } public bool GetButtonDown(Button b) { return buttons_down[(int)b]; } public bool GetButton(Button b) { return buttons[(int)b]; } public bool GetButtonUp(Button b) { return buttons_up[(int)b]; } public float[] GetStick() { return stick; } public float[] GetStick2() { return stick2; } public Vector3 GetGyro() { return gyr_g; } public Vector3 GetAccel() { return acc_g; } public int Attach() { state = state_.ATTACHED; // Make sure command is received HIDapi.hid_set_nonblocking(handle, 0); byte[] a = { 0x0 }; // Connect if (isUSB) { a = Enumerable.Repeat((byte)0, 64).ToArray(); form.AppendTextBox("Using USB.\r\n"); a[0] = 0x80; a[1] = 0x1; HIDapi.hid_write(handle, a, new UIntPtr(2)); HIDapi.hid_read_timeout(handle, a, new UIntPtr(64), 100); if (a[0] != 0x81) { // can occur when USB connection isn't closed properly form.AppendTextBox("Resetting USB connection.\r\n"); Subcommand(0x06, new byte[] { 0x01 }, 1); throw new Exception("reset_usb"); } if (a[3] == 0x3) { PadMacAddress = new PhysicalAddress(new byte[] { a[9], a[8], a[7], a[6], a[5], a[4] }); } // USB Pairing a = Enumerable.Repeat((byte)0, 64).ToArray(); a[0] = 0x80; a[1] = 0x2; // Handshake HIDapi.hid_write(handle, a, new UIntPtr(2)); HIDapi.hid_read_timeout(handle, a, new UIntPtr(64), 100); a[0] = 0x80; a[1] = 0x3; // 3Mbit baud rate HIDapi.hid_write(handle, a, new UIntPtr(2)); HIDapi.hid_read_timeout(handle, a, new UIntPtr(64), 100); a[0] = 0x80; a[1] = 0x2; // Handshake at new baud rate HIDapi.hid_write(handle, a, new UIntPtr(2)); HIDapi.hid_read_timeout(handle, a, new UIntPtr(64), 100); a[0] = 0x80; a[1] = 0x4; // Prevent HID timeout HIDapi.hid_write(handle, a, new UIntPtr(2)); // doesn't actually prevent timout... HIDapi.hid_read_timeout(handle, a, new UIntPtr(64), 100); } dump_calibration_data(); // Bluetooth manual pairing byte[] btmac_host = Program.btMAC.GetAddressBytes(); // send host MAC and acquire Joycon MAC //byte[] reply = Subcommand(0x01, new byte[] { 0x01, btmac_host[5], btmac_host[4], btmac_host[3], btmac_host[2], btmac_host[1], btmac_host[0] }, 7, true); //byte[] LTKhash = Subcommand(0x01, new byte[] { 0x02 }, 1, true); // save pairing info //Subcommand(0x01, new byte[] { 0x03 }, 1, true); BlinkHomeLight(); SetLEDByPlayerNum(PadId); Subcommand(0x40, new byte[] { (imu_enabled ? (byte)0x1 : (byte)0x0) }, 1); Subcommand(0x48, new byte[] { 0x01 }, 1); Subcommand(0x3, new byte[] { 0x30 }, 1); DebugPrint("Done with init.", DebugType.COMMS); HIDapi.hid_set_nonblocking(handle, 1); return 0; } public void SetPlayerLED(byte leds_ = 0x0) { Subcommand(0x30, new byte[] { leds_ }, 1); } public void BlinkHomeLight() { // do not call after initial setup if (thirdParty) return; byte[] a = Enumerable.Repeat((byte)0xFF, 25).ToArray(); a[0] = 0x18; a[1] = 0x01; Subcommand(0x38, a, 25); } public void SetHomeLight(bool on) { if (thirdParty) return; byte[] a = Enumerable.Repeat((byte)0xFF, 25).ToArray(); if (on) { a[0] = 0x1F; a[1] = 0xF0; } else { a[0] = 0x10; a[1] = 0x01; } Subcommand(0x38, a, 25); } private void SetHCIState(byte state) { byte[] a = { state }; Subcommand(0x06, a, 1); } public void PowerOff() { if (state > state_.DROPPED) { HIDapi.hid_set_nonblocking(handle, 0); SetHCIState(0x00); state = state_.DROPPED; } } private void BatteryChanged() { // battery changed level foreach (var v in form.con) { if (v.Tag == this) { switch (battery) { case 4: v.BackColor = System.Drawing.Color.FromArgb(0xAA, System.Drawing.Color.Green); break; case 3: v.BackColor = System.Drawing.Color.FromArgb(0xAA, System.Drawing.Color.Green); break; case 2: v.BackColor = System.Drawing.Color.FromArgb(0xAA, System.Drawing.Color.GreenYellow); break; case 1: v.BackColor = System.Drawing.Color.FromArgb(0xAA, System.Drawing.Color.Orange); break; default: v.BackColor = System.Drawing.Color.FromArgb(0xAA, System.Drawing.Color.Red); break; } } } if (battery <= 1) { form.notifyIcon.Visible = true; form.notifyIcon.BalloonTipText = String.Format("Controller {0} ({1}) - low battery notification!", PadId, isPro ? "Pro Controller" : (isSnes ? "SNES Controller" : (is64? "N64 Controller" : (isLeft ? "Joycon Left" : "Joycon Right")))); form.notifyIcon.ShowBalloonTip(0); } } public void SetFilterCoeff(float a) { filterweight = a; } public void Detach(bool close = false) { stop_polling = true; if (out_xbox != null) { out_xbox.Disconnect(); } if (out_ds4 != null) { out_ds4.Disconnect(); } if (state > state_.NO_JOYCONS) { HIDapi.hid_set_nonblocking(handle, 0); // Subcommand(0x40, new byte[] { 0x0 }, 1); // disable IMU sensor //Subcommand(0x48, new byte[] { 0x0 }, 1); // Would turn off rumble? if (isUSB) { byte[] a = Enumerable.Repeat((byte)0, 64).ToArray(); a[0] = 0x80; a[1] = 0x5; // Allow device to talk to BT again HIDapi.hid_write(handle, a, new UIntPtr(2)); a[0] = 0x80; a[1] = 0x6; // Allow device to talk to BT again HIDapi.hid_write(handle, a, new UIntPtr(2)); } } if (close || state > state_.DROPPED) { HIDapi.hid_close(handle); } state = state_.NOT_ATTACHED; } private byte ts_en; private int ReceiveRaw() { if (handle == IntPtr.Zero) return -2; byte[] raw_buf = new byte[report_len]; int ret = HIDapi.hid_read_timeout(handle, raw_buf, new UIntPtr(report_len), 5); if (ret > 0) { // Process packets as soon as they come for (int n = 0; n < 3; n++) { ExtractIMUValues(raw_buf, n); byte lag = (byte)Math.Max(0, raw_buf[1] - ts_en - 3); if (n == 0) { Timestamp += (ulong)lag * 5000; // add lag once ProcessButtonsAndStick(raw_buf); // process buttons here to have them affect DS4 DoThingsWithButtons(); int newbat = battery; battery = (raw_buf[2] >> 4) / 2; if (newbat != battery) BatteryChanged(); } Timestamp += 5000; // 5ms difference packetCounter++; if (Program.server != null) Program.server.NewReportIncoming(this); if (out_ds4 != null) { try { out_ds4.UpdateInput(MapToDualShock4Input(this)); } catch (Exception e) { // ignore /shrug } } } // no reason to send XInput reports so often if (out_xbox != null) { try { out_xbox.UpdateInput(MapToXbox360Input(this)); } catch (Exception e) { // ignore /shrug } } if (ts_en == raw_buf[1] && !(isSnes || is64)) { form.AppendTextBox("Duplicate timestamp enqueued.\r\n"); DebugPrint(string.Format("Duplicate timestamp enqueued. TS: {0:X2}", ts_en), DebugType.THREADING); } ts_en = raw_buf[1]; DebugPrint(string.Format("Enqueue. Bytes read: {0:D}. Timestamp: {1:X2}", ret, raw_buf[1]), DebugType.THREADING); } return ret; } private readonly Stopwatch shakeTimer = Stopwatch.StartNew(); //Setup a timer for measuring shake in milliseconds private long shakedTime = 0; private bool hasShaked; void DetectShake() { if (form.shakeInputEnabled) { long currentShakeTime = shakeTimer.ElapsedMilliseconds; // Shake detection logic bool isShaking = GetAccel().LengthSquared() >= form.shakeSesitivity; if (isShaking && currentShakeTime >= shakedTime + form.shakeDelay || isShaking && shakedTime == 0) { shakedTime = currentShakeTime; hasShaked = true; // Mapped shake key down Simulate(Config.Value("shake"), false, false); DebugPrint("Shaked at time: " + shakedTime.ToString(), DebugType.SHAKE); } // If controller was shaked then release mapped key after a small delay to simulate a button press, then reset hasShaked if (hasShaked && currentShakeTime >= shakedTime + 10) { // Mapped shake key up Simulate(Config.Value("shake"), false, true); DebugPrint("Shake completed", DebugType.SHAKE); hasShaked = false; } } else { shakeTimer.Stop(); return; } } bool dragToggle = Boolean.Parse(ConfigurationManager.AppSettings["DragToggle"]); Dictionary mouse_toggle_btn = new Dictionary(); private void Simulate(string s, bool click = true, bool up = false) { if (s.StartsWith("key_")) { WindowsInput.Events.KeyCode key = (WindowsInput.Events.KeyCode)Int32.Parse(s.Substring(4)); if (click) { WindowsInput.Simulate.Events().Click(key).Invoke(); } else { if (up) { WindowsInput.Simulate.Events().Release(key).Invoke(); } else { WindowsInput.Simulate.Events().Hold(key).Invoke(); } } } else if (s.StartsWith("mse_")) { WindowsInput.Events.ButtonCode button = (WindowsInput.Events.ButtonCode)Int32.Parse(s.Substring(4)); if (click) { WindowsInput.Simulate.Events().Click(button).Invoke(); } else { if (dragToggle) { if (!up) { bool release; mouse_toggle_btn.TryGetValue((int)button, out release); if (release) WindowsInput.Simulate.Events().Release(button).Invoke(); else WindowsInput.Simulate.Events().Hold(button).Invoke(); mouse_toggle_btn[(int)button] = !release; } } else { if (up) { WindowsInput.Simulate.Events().Release(button).Invoke(); } else { WindowsInput.Simulate.Events().Hold(button).Invoke(); } } } } } // For Joystick->Joystick inputs private void SimulateContinous(int origin, string s) { if (s.StartsWith("joy_")) { int button = Int32.Parse(s.Substring(4)); buttons[button] |= buttons[origin]; } } bool HomeLongPowerOff = Boolean.Parse(ConfigurationManager.AppSettings["HomeLongPowerOff"]); long PowerOffInactivityMins = Int32.Parse(ConfigurationManager.AppSettings["PowerOffInactivity"]); bool ChangeOrientationDoubleClick = Boolean.Parse(ConfigurationManager.AppSettings["ChangeOrientationDoubleClick"]); long lastDoubleClick = -1; string extraGyroFeature = ConfigurationManager.AppSettings["GyroToJoyOrMouse"]; bool UseFilteredIMU = Boolean.Parse(ConfigurationManager.AppSettings["UseFilteredIMU"]); int GyroMouseSensitivityX = Int32.Parse(ConfigurationManager.AppSettings["GyroMouseSensitivityX"]); int GyroMouseSensitivityY = Int32.Parse(ConfigurationManager.AppSettings["GyroMouseSensitivityY"]); float GyroStickSensitivityX = float.Parse(ConfigurationManager.AppSettings["GyroStickSensitivityX"]); float GyroStickSensitivityY = float.Parse(ConfigurationManager.AppSettings["GyroStickSensitivityY"]); float GyroStickReduction = float.Parse(ConfigurationManager.AppSettings["GyroStickReduction"]); bool GyroHoldToggle = Boolean.Parse(ConfigurationManager.AppSettings["GyroHoldToggle"]); bool GyroAnalogSliders = Boolean.Parse(ConfigurationManager.AppSettings["GyroAnalogSliders"]); int GyroAnalogSensitivity = Int32.Parse(ConfigurationManager.AppSettings["GyroAnalogSensitivity"]); byte[] sliderVal = new byte[] { 0, 0 }; private void DoThingsWithButtons() { int powerOffButton = (int)((isPro || !isLeft || other != null) ? Button.HOME : Button.CAPTURE); long timestamp = Stopwatch.GetTimestamp(); if (HomeLongPowerOff && buttons[powerOffButton]) { if ((timestamp - buttons_down_timestamp[powerOffButton]) / 10000 > 2000.0) { if (other != null) other.PowerOff(); PowerOff(); return; } } if (ChangeOrientationDoubleClick && buttons_down[(int)Button.STICK] && lastDoubleClick != -1 && !isPro) { if ((buttons_down_timestamp[(int)Button.STICK] - lastDoubleClick) < 3000000) { form.conBtnClick(form.con[PadId], EventArgs.Empty); // trigger connection button click lastDoubleClick = buttons_down_timestamp[(int)Button.STICK]; return; } lastDoubleClick = buttons_down_timestamp[(int)Button.STICK]; } else if (ChangeOrientationDoubleClick && buttons_down[(int)Button.STICK] && !isPro) { lastDoubleClick = buttons_down_timestamp[(int)Button.STICK]; } if (PowerOffInactivityMins > 0) { if ((timestamp - inactivity) / 10000 > PowerOffInactivityMins * 60 * 1000) { if (other != null) other.PowerOff(); PowerOff(); return; } } DetectShake(); if (buttons_down[(int)Button.CAPTURE]) Simulate(Config.Value("capture")); if (buttons_down[(int)Button.HOME]) Simulate(Config.Value("home")); SimulateContinous((int)Button.CAPTURE, Config.Value("capture")); SimulateContinous((int)Button.HOME, Config.Value("home")); if (isLeft) { if (buttons_down[(int)Button.SL]) Simulate(Config.Value("sl_l"), false, false); if (buttons_up[(int)Button.SL]) Simulate(Config.Value("sl_l"), false, true); if (buttons_down[(int)Button.SR]) Simulate(Config.Value("sr_l"), false, false); if (buttons_up[(int)Button.SR]) Simulate(Config.Value("sr_l"), false, true); SimulateContinous((int)Button.SL, Config.Value("sl_l")); SimulateContinous((int)Button.SR, Config.Value("sr_l")); } else { if (buttons_down[(int)Button.SL]) Simulate(Config.Value("sl_r"), false, false); if (buttons_up[(int)Button.SL]) Simulate(Config.Value("sl_r"), false, true); if (buttons_down[(int)Button.SR]) Simulate(Config.Value("sr_r"), false, false); if (buttons_up[(int)Button.SR]) Simulate(Config.Value("sr_r"), false, true); SimulateContinous((int)Button.SL, Config.Value("sl_r")); SimulateContinous((int)Button.SR, Config.Value("sr_r")); } // Filtered IMU data this.cur_rotation = AHRS.GetEulerAngles(); float dt = 0.015f; // 15ms if (GyroAnalogSliders && (other != null || isPro)) { Button leftT = isLeft ? Button.SHOULDER_2 : Button.SHOULDER2_2; Button rightT = isLeft ? Button.SHOULDER2_2 : Button.SHOULDER_2; Joycon left = isLeft ? this : (isPro ? this : this.other); Joycon right = !isLeft ? this : (isPro ? this : this.other); int ldy, rdy; if (UseFilteredIMU) { ldy = (int)(GyroAnalogSensitivity * (left.cur_rotation[0] - left.cur_rotation[3])); rdy = (int)(GyroAnalogSensitivity * (right.cur_rotation[0] - right.cur_rotation[3])); } else { ldy = (int)(GyroAnalogSensitivity * (left.gyr_g.Y * dt)); rdy = (int)(GyroAnalogSensitivity * (right.gyr_g.Y * dt)); } if (buttons[(int)leftT]) { sliderVal[0] = (byte)Math.Min(Byte.MaxValue, Math.Max(0, (int)sliderVal[0] + ldy)); } else { sliderVal[0] = 0; } if (buttons[(int)rightT]) { sliderVal[1] = (byte)Math.Min(Byte.MaxValue, Math.Max(0, (int)sliderVal[1] + rdy)); } else { sliderVal[1] = 0; } } string res_val = Config.Value("active_gyro"); if (res_val.StartsWith("joy_")) { int i = Int32.Parse(res_val.Substring(4)); if (GyroHoldToggle) { if (buttons_down[i] || (other != null && other.buttons_down[i])) active_gyro = true; else if (buttons_up[i] || (other != null && other.buttons_up[i])) active_gyro = false; } else { if (buttons_down[i] || (other != null && other.buttons_down[i])) active_gyro = !active_gyro; } } if (extraGyroFeature.Substring(0, 3) == "joy") { if (Config.Value("active_gyro") == "0" || active_gyro) { float[] control_stick = (extraGyroFeature == "joy_left") ? stick : stick2; float dx, dy; if (UseFilteredIMU) { dx = (GyroStickSensitivityX * (cur_rotation[1] - cur_rotation[4])); // yaw dy = -(GyroStickSensitivityY * (cur_rotation[0] - cur_rotation[3])); // pitch } else { dx = (GyroStickSensitivityX * (gyr_g.Z * dt)); // yaw dy = -(GyroStickSensitivityY * (gyr_g.Y * dt)); // pitch } control_stick[0] = Math.Max(-1.0f, Math.Min(1.0f, control_stick[0] / GyroStickReduction + dx)); control_stick[1] = Math.Max(-1.0f, Math.Min(1.0f, control_stick[1] / GyroStickReduction + dy)); } } else if (extraGyroFeature == "mouse" && (isPro || (other == null) || (other != null && (Boolean.Parse(ConfigurationManager.AppSettings["GyroMouseLeftHanded"]) ? isLeft : !isLeft)))) { // gyro data is in degrees/s if (Config.Value("active_gyro") == "0" || active_gyro) { int dx, dy; if (UseFilteredIMU) { dx = (int)(GyroMouseSensitivityX * (cur_rotation[1] - cur_rotation[4])); // yaw dy = (int)-(GyroMouseSensitivityY * (cur_rotation[0] - cur_rotation[3])); // pitch } else { dx = (int)(GyroMouseSensitivityX * (gyr_g.Z * dt)); dy = (int)-(GyroMouseSensitivityY * (gyr_g.Y * dt)); } WindowsInput.Simulate.Events().MoveBy(dx, dy).Invoke(); } // reset mouse position to centre of primary monitor res_val = Config.Value("reset_mouse"); if (res_val.StartsWith("joy_")) { int i = Int32.Parse(res_val.Substring(4)); if (buttons_down[i] || (other != null && other.buttons_down[i])) WindowsInput.Simulate.Events().MoveTo(Screen.PrimaryScreen.Bounds.Width / 2, Screen.PrimaryScreen.Bounds.Height / 2).Invoke(); } } } private Thread PollThreadObj; private void Poll() { stop_polling = false; int attempts = 0; while (!stop_polling & state > state_.NO_JOYCONS) { if (rumble_obj.queue.Count > 0) { SendRumble(rumble_obj.GetData()); } int a = ReceiveRaw(); if (a > 0 && state > state_.DROPPED) { state = state_.IMU_DATA_OK; attempts = 0; } else if (attempts > 240) { state = state_.DROPPED; form.AppendTextBox("Dropped.\r\n"); DebugPrint("Connection lost. Is the Joy-Con connected?", DebugType.ALL); break; } else if (a < 0) { // An error on read. //form.AppendTextBox("Pause 5ms"); Thread.Sleep((Int32)5); ++attempts; } else if (a == 0) { // The non-blocking read timed out. No need to sleep. // No need to increase attempts because it's not an error. } } } public float[] otherStick = { 0, 0 }; bool swapAB = Boolean.Parse(ConfigurationManager.AppSettings["SwapAB"]); bool swapXY = Boolean.Parse(ConfigurationManager.AppSettings["SwapXY"]); bool realn64Range = Boolean.Parse(ConfigurationManager.AppSettings["N64Range"]); float stickScalingFactor = float.Parse(ConfigurationManager.AppSettings["StickScalingFactor"]); float stickScalingFactor2 = float.Parse(ConfigurationManager.AppSettings["StickScalingFactor2"]); private int ProcessButtonsAndStick(byte[] report_buf) { if (report_buf[0] == 0x00) throw new ArgumentException("received undefined report. This is probably a bug"); if (!isSnes) { stick_raw[0] = report_buf[6 + (isLeft ? 0 : 3)]; stick_raw[1] = report_buf[7 + (isLeft ? 0 : 3)]; stick_raw[2] = report_buf[8 + (isLeft ? 0 : 3)]; if (isPro) { stick2_raw[0] = report_buf[6 + (!isLeft ? 0 : 3)]; stick2_raw[1] = report_buf[7 + (!isLeft ? 0 : 3)]; stick2_raw[2] = report_buf[8 + (!isLeft ? 0 : 3)]; } stick_precal[0] = (UInt16)(stick_raw[0] | ((stick_raw[1] & 0xf) << 8)); stick_precal[1] = (UInt16)((stick_raw[1] >> 4) | (stick_raw[2] << 4)); stick = CenterSticks(stick_precal, stick_cal, deadzone, isLeft ? stickScalingFactor : stickScalingFactor2); if (isPro) { stick2_precal[0] = (UInt16)(stick2_raw[0] | ((stick2_raw[1] & 0xf) << 8)); stick2_precal[1] = (UInt16)((stick2_raw[1] >> 4) | (stick2_raw[2] << 4)); stick2 = CenterSticks(stick2_precal, stick2_cal, deadzone2, stickScalingFactor2); } // Read other Joycon's sticks if (isLeft && other != null && other != this) { stick2 = otherStick; other.otherStick = stick; } if (!isLeft && other != null && other != this) { Array.Copy(stick, stick2, 2); stick = otherStick; other.otherStick = stick2; } } // // Set button states both for server and ViGEm lock (buttons) { lock (down_) { for (int i = 0; i < buttons.Length; ++i) { down_[i] = buttons[i]; } } buttons = new bool[20]; buttons[(int)Button.DPAD_DOWN] = (report_buf[3 + (isLeft ? 2 : 0)] & (isLeft ? 0x01 : 0x04)) != 0; buttons[(int)Button.DPAD_RIGHT] = (report_buf[3 + (isLeft ? 2 : 0)] & (isLeft ? 0x04 : 0x08)) != 0; buttons[(int)Button.DPAD_UP] = (report_buf[3 + (isLeft ? 2 : 0)] & (isLeft ? 0x02 : 0x02)) != 0; buttons[(int)Button.DPAD_LEFT] = (report_buf[3 + (isLeft ? 2 : 0)] & (isLeft ? 0x08 : 0x01)) != 0; buttons[(int)Button.HOME] = ((report_buf[4] & 0x10) != 0); buttons[(int)Button.CAPTURE] = ((report_buf[4] & 0x20) != 0); buttons[(int)Button.MINUS] = ((report_buf[4] & 0x01) != 0); buttons[(int)Button.PLUS] = ((report_buf[4] & 0x02) != 0); buttons[(int)Button.STICK] = ((report_buf[4] & (isLeft ? 0x08 : 0x04)) != 0); buttons[(int)Button.SHOULDER_1] = (report_buf[3 + (isLeft ? 2 : 0)] & 0x40) != 0; buttons[(int)Button.SHOULDER_2] = (report_buf[3 + (isLeft ? 2 : 0)] & 0x80) != 0; buttons[(int)Button.SR] = (report_buf[3 + (isLeft ? 2 : 0)] & 0x10) != 0; buttons[(int)Button.SL] = (report_buf[3 + (isLeft ? 2 : 0)] & 0x20) != 0; if (isPro) { buttons[(int)Button.B] = (report_buf[3 + (!isLeft ? 2 : 0)] & (!isLeft ? 0x01 : 0x04)) != 0; buttons[(int)Button.A] = (report_buf[3 + (!isLeft ? 2 : 0)] & (!isLeft ? 0x04 : 0x08)) != 0; buttons[(int)Button.X] = (report_buf[3 + (!isLeft ? 2 : 0)] & (!isLeft ? 0x02 : 0x02)) != 0; buttons[(int)Button.Y] = (report_buf[3 + (!isLeft ? 2 : 0)] & (!isLeft ? 0x08 : 0x01)) != 0; buttons[(int)Button.STICK2] = ((report_buf[4] & (!isLeft ? 0x08 : 0x04)) != 0); buttons[(int)Button.SHOULDER2_1] = (report_buf[3 + (!isLeft ? 2 : 0)] & 0x40) != 0; buttons[(int)Button.SHOULDER2_2] = (report_buf[3 + (!isLeft ? 2 : 0)] & 0x80) != 0; } if (other != null && other != this) { buttons[(int)(Button.B)] = other.buttons[(int)Button.DPAD_DOWN]; buttons[(int)(Button.A)] = other.buttons[(int)Button.DPAD_RIGHT]; buttons[(int)(Button.X)] = other.buttons[(int)Button.DPAD_UP]; buttons[(int)(Button.Y)] = other.buttons[(int)Button.DPAD_LEFT]; buttons[(int)Button.STICK2] = other.buttons[(int)Button.STICK]; buttons[(int)Button.SHOULDER2_1] = other.buttons[(int)Button.SHOULDER_1]; buttons[(int)Button.SHOULDER2_2] = other.buttons[(int)Button.SHOULDER_2]; } if (isLeft && other != null && other != this) { buttons[(int)Button.HOME] = other.buttons[(int)Button.HOME]; buttons[(int)Button.PLUS] = other.buttons[(int)Button.PLUS]; } if (!isLeft && other != null && other != this) { buttons[(int)Button.MINUS] = other.buttons[(int)Button.MINUS]; } long timestamp = Stopwatch.GetTimestamp(); lock (buttons_up) { lock (buttons_down) { bool changed = false; for (int i = 0; i < buttons.Length; ++i) { buttons_up[i] = (down_[i] & !buttons[i]); buttons_down[i] = (!down_[i] & buttons[i]); if (down_[i] != buttons[i]) buttons_down_timestamp[i] = (buttons[i] ? timestamp : -1); if (buttons_up[i] || buttons_down[i]) changed = true; } inactivity = (changed) ? timestamp : inactivity; } } } return 0; } // Get Gyro/Accel data private void ExtractIMUValues(byte[] report_buf, int n = 0) { if (!(isSnes || is64)) { gyr_r[0] = (Int16)(report_buf[19 + n * 12] | ((report_buf[20 + n * 12] << 8) & 0xff00)); gyr_r[1] = (Int16)(report_buf[21 + n * 12] | ((report_buf[22 + n * 12] << 8) & 0xff00)); gyr_r[2] = (Int16)(report_buf[23 + n * 12] | ((report_buf[24 + n * 12] << 8) & 0xff00)); acc_r[0] = (Int16)(report_buf[13 + n * 12] | ((report_buf[14 + n * 12] << 8) & 0xff00)); acc_r[1] = (Int16)(report_buf[15 + n * 12] | ((report_buf[16 + n * 12] << 8) & 0xff00)); acc_r[2] = (Int16)(report_buf[17 + n * 12] | ((report_buf[18 + n * 12] << 8) & 0xff00)); if (form.allowCalibration) { for (int i = 0; i < 3; ++i) { switch (i) { case 0: acc_g.X = (acc_r[i] - activeData[3]) * (1.0f / acc_sen[i]) * 4.0f; gyr_g.X = (gyr_r[i] - activeData[0]) * (816.0f / gyr_sen[i]); if (form.calibrate) { form.xA.Add(acc_r[i]); form.xG.Add(gyr_r[i]); } break; case 1: acc_g.Y = (!isLeft ? -1 : 1) * (acc_r[i] - activeData[4]) * (1.0f / acc_sen[i]) * 4.0f; gyr_g.Y = -(!isLeft ? -1 : 1) * (gyr_r[i] - activeData[1]) * (816.0f / gyr_sen[i]); if (form.calibrate) { form.yA.Add(acc_r[i]); form.yG.Add(gyr_r[i]); } break; case 2: acc_g.Z = (!isLeft ? -1 : 1) * (acc_r[i] - activeData[5]) * (1.0f / acc_sen[i]) * 4.0f; gyr_g.Z = -(!isLeft ? -1 : 1) * (gyr_r[i] - activeData[2]) * (816.0f / gyr_sen[i]); if (form.calibrate) { form.zA.Add(acc_r[i]); form.zG.Add(gyr_r[i]); } break; } } } else { Int16[] offset; if (isPro) offset = pro_hor_offset; else if (isLeft) offset = left_hor_offset; else offset = right_hor_offset; for (int i = 0; i < 3; ++i) { switch (i) { case 0: acc_g.X = (acc_r[i] - offset[i]) * (1.0f / (acc_sensiti[i] - acc_neutral[i])) * 4.0f; gyr_g.X = (gyr_r[i] - gyr_neutral[i]) * (816.0f / (gyr_sensiti[i] - gyr_neutral[i])); break; case 1: acc_g.Y = (!isLeft ? -1 : 1) * (acc_r[i] - offset[i]) * (1.0f / (acc_sensiti[i] - acc_neutral[i])) * 4.0f; gyr_g.Y = -(!isLeft ? -1 : 1) * (gyr_r[i] - gyr_neutral[i]) * (816.0f / (gyr_sensiti[i] - gyr_neutral[i])); break; case 2: acc_g.Z = (!isLeft ? -1 : 1) * (acc_r[i] - offset[i]) * (1.0f / (acc_sensiti[i] - acc_neutral[i])) * 4.0f; gyr_g.Z = -(!isLeft ? -1 : 1) * (gyr_r[i] - gyr_neutral[i]) * (816.0f / (gyr_sensiti[i] - gyr_neutral[i])); break; } } } if (other == null && !isPro) { // single joycon mode; Z do not swap, rest do if (isLeft) { acc_g.X = -acc_g.X; acc_g.Y = -acc_g.Y; gyr_g.X = -gyr_g.X; } else { gyr_g.Y = -gyr_g.Y; } float temp = acc_g.X; acc_g.X = acc_g.Y; acc_g.Y = -temp; temp = gyr_g.X; gyr_g.X = gyr_g.Y; gyr_g.Y = temp; } // Update rotation Quaternion float deg_to_rad = 0.0174533f; AHRS.Update(gyr_g.X * deg_to_rad, gyr_g.Y * deg_to_rad, gyr_g.Z * deg_to_rad, acc_g.X, acc_g.Y, acc_g.Z); } } public void Begin() { if (PollThreadObj == null) { PollThreadObj = new Thread(new ThreadStart(Poll)); PollThreadObj.IsBackground = true; PollThreadObj.Start(); form.AppendTextBox("Starting poll thread.\r\n"); } else { form.AppendTextBox("Poll cannot start.\r\n"); } } // Should really be called calculating stick data private float[] CenterSticks(UInt16[] vals, ushort[] cal, ushort dz, float scaling_factor) { ushort[] t = cal; float[] s = { 0, 0 }; float dx = vals[0] - t[2], dy = vals[1] - t[3]; if (Math.Abs(dx * dx + dy * dy) < dz * dz) return s; s[0] = dx / (dx > 0 ? t[0] : t[4]); s[1] = dy / (dy > 0 ? t[1] : t[5]); if (scaling_factor != 1.0f) { s[0] *= scaling_factor; s[1] *= scaling_factor; s[0] = Math.Max(Math.Min(s[0], 1.0f), -1.0f); s[1] = Math.Max(Math.Min(s[1], 1.0f), -1.0f); } return s; } private static short CastStickValue(float stick_value) { return (short)Math.Max(Int16.MinValue, Math.Min(Int16.MaxValue, stick_value * (stick_value > 0 ? Int16.MaxValue : -Int16.MinValue))); } private static byte CastStickValueByte(float stick_value) { return (byte)Math.Max(Byte.MinValue, Math.Min(Byte.MaxValue, 127 - stick_value * Byte.MaxValue)); } public void SetRumble(float low_freq, float high_freq, float amp) { if (state <= Joycon.state_.ATTACHED) return; rumble_obj.set_vals(low_freq, high_freq, amp); } private void SendRumble(byte[] buf) { byte[] buf_ = new byte[report_len]; buf_[0] = 0x10; buf_[1] = global_count; if (global_count == 0xf) global_count = 0; else ++global_count; Array.Copy(buf, 0, buf_, 2, 8); PrintArray(buf_, DebugType.RUMBLE, format: "Rumble data sent: {0:S}"); HIDapi.hid_write(handle, buf_, new UIntPtr(report_len)); } private byte[] Subcommand(byte sc, byte[] buf, uint len, bool print = true) { byte[] buf_ = new byte[report_len]; byte[] response = new byte[report_len]; Array.Copy(default_buf, 0, buf_, 2, 8); Array.Copy(buf, 0, buf_, 11, len); buf_[10] = sc; buf_[1] = global_count; buf_[0] = 0x1; if (global_count == 0xf) global_count = 0; else ++global_count; if (print) { PrintArray(buf_, DebugType.COMMS, len, 11, "Subcommand 0x" + string.Format("{0:X2}", sc) + " sent. Data: 0x{0:S}"); }; HIDapi.hid_write(handle, buf_, new UIntPtr(len + 11)); int tries = 0; do { int res = HIDapi.hid_read_timeout(handle, response, new UIntPtr(report_len), 100); if (res < 1) DebugPrint("No response.", DebugType.COMMS); else if (print) { PrintArray(response, DebugType.COMMS, report_len - 1, 1, "Response ID 0x" + string.Format("{0:X2}", response[0]) + ". Data: 0x{0:S}"); } tries++; } while (tries < 10 && response[0] != 0x21 && response[14] != sc); return response; } private void dump_calibration_data() { if (isSnes || is64 || thirdParty) { short[] temp = (short[])ConfigurationManager.AppSettings["acc_sensiti"].Split(',').Select(s => short.Parse(s)).ToArray(); acc_sensiti[0] = temp[0]; acc_sensiti[1] = temp[1]; acc_sensiti[2] = temp[2]; temp = (short[])ConfigurationManager.AppSettings["gyr_sensiti"].Split(',').Select(s => short.Parse(s)).ToArray(); gyr_sensiti[0] = temp[0]; gyr_sensiti[1] = temp[1]; gyr_sensiti[2] = temp[2]; ushort[] temp2 = (ushort[])ConfigurationManager.AppSettings["stick_cal"].Split(',').Select(s => ushort.Parse(s.Substring(2), System.Globalization.NumberStyles.HexNumber)).ToArray(); stick_cal[0] = temp2[0]; stick_cal[1] = temp2[1]; stick_cal[2] = temp2[2]; stick_cal[3] = temp2[3]; stick_cal[4] = temp2[4]; stick_cal[5] = temp2[5]; deadzone = ushort.Parse(ConfigurationManager.AppSettings["deadzone"]); temp2 = (ushort[])ConfigurationManager.AppSettings["stick2_cal"].Split(',').Select(s => ushort.Parse(s.Substring(2), System.Globalization.NumberStyles.HexNumber)).ToArray(); stick2_cal[0] = temp2[0]; stick2_cal[1] = temp2[1]; stick2_cal[2] = temp2[2]; stick2_cal[3] = temp2[3]; stick2_cal[4] = temp2[4]; stick2_cal[5] = temp2[5]; deadzone2 = ushort.Parse(ConfigurationManager.AppSettings["deadzone2"]); return; } HIDapi.hid_set_nonblocking(handle, 0); byte[] buf_ = ReadSPI(0x80, (isLeft ? (byte)0x12 : (byte)0x1d), 9); // get user calibration data if possible bool found = false; for (int i = 0; i < 9; ++i) { if (buf_[i] != 0xff) { form.AppendTextBox("Using user stick calibration data.\r\n"); found = true; break; } } if (!found) { form.AppendTextBox("Using factory stick calibration data.\r\n"); buf_ = ReadSPI(0x60, (isLeft ? (byte)0x3d : (byte)0x46), 9); // get user calibration data if possible } stick_cal[isLeft ? 0 : 2] = (UInt16)((buf_[1] << 8) & 0xF00 | buf_[0]); // X Axis Max above center stick_cal[isLeft ? 1 : 3] = (UInt16)((buf_[2] << 4) | (buf_[1] >> 4)); // Y Axis Max above center stick_cal[isLeft ? 2 : 4] = (UInt16)((buf_[4] << 8) & 0xF00 | buf_[3]); // X Axis Center stick_cal[isLeft ? 3 : 5] = (UInt16)((buf_[5] << 4) | (buf_[4] >> 4)); // Y Axis Center stick_cal[isLeft ? 4 : 0] = (UInt16)((buf_[7] << 8) & 0xF00 | buf_[6]); // X Axis Min below center stick_cal[isLeft ? 5 : 1] = (UInt16)((buf_[8] << 4) | (buf_[7] >> 4)); // Y Axis Min below center PrintArray(stick_cal, len: 6, start: 0, format: "Stick calibration data: {0:S}"); if (isPro) { buf_ = ReadSPI(0x80, (!isLeft ? (byte)0x12 : (byte)0x1d), 9); // get user calibration data if possible found = false; for (int i = 0; i < 9; ++i) { if (buf_[i] != 0xff) { form.AppendTextBox("Using user stick calibration data.\r\n"); found = true; break; } } if (!found) { form.AppendTextBox("Using factory stick calibration data.\r\n"); buf_ = ReadSPI(0x60, (!isLeft ? (byte)0x3d : (byte)0x46), 9); // get user calibration data if possible } stick2_cal[!isLeft ? 0 : 2] = (UInt16)((buf_[1] << 8) & 0xF00 | buf_[0]); // X Axis Max above center stick2_cal[!isLeft ? 1 : 3] = (UInt16)((buf_[2] << 4) | (buf_[1] >> 4)); // Y Axis Max above center stick2_cal[!isLeft ? 2 : 4] = (UInt16)((buf_[4] << 8) & 0xF00 | buf_[3]); // X Axis Center stick2_cal[!isLeft ? 3 : 5] = (UInt16)((buf_[5] << 4) | (buf_[4] >> 4)); // Y Axis Center stick2_cal[!isLeft ? 4 : 0] = (UInt16)((buf_[7] << 8) & 0xF00 | buf_[6]); // X Axis Min below center stick2_cal[!isLeft ? 5 : 1] = (UInt16)((buf_[8] << 4) | (buf_[7] >> 4)); // Y Axis Min below center PrintArray(stick2_cal, len: 6, start: 0, format: "Stick calibration data: {0:S}"); buf_ = ReadSPI(0x60, (!isLeft ? (byte)0x86 : (byte)0x98), 16); deadzone2 = (UInt16)((buf_[4] << 8) & 0xF00 | buf_[3]); } buf_ = ReadSPI(0x60, (isLeft ? (byte)0x86 : (byte)0x98), 16); deadzone = (UInt16)((buf_[4] << 8) & 0xF00 | buf_[3]); buf_ = ReadSPI(0x80, 0x28, 10); acc_neutral[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); acc_neutral[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); acc_neutral[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x80, 0x2E, 10); acc_sensiti[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); acc_sensiti[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); acc_sensiti[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x80, 0x34, 10); gyr_neutral[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); gyr_neutral[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); gyr_neutral[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x80, 0x3A, 10); gyr_sensiti[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); gyr_sensiti[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); gyr_sensiti[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); PrintArray(gyr_neutral, len: 3, d: DebugType.IMU, format: "User gyro neutral position: {0:S}"); // This is an extremely messy way of checking to see whether there is user stick calibration data present, but I've seen conflicting user calibration data on blank Joy-Cons. Worth another look eventually. if (gyr_neutral[0] + gyr_neutral[1] + gyr_neutral[2] == -3 || Math.Abs(gyr_neutral[0]) > 100 || Math.Abs(gyr_neutral[1]) > 100 || Math.Abs(gyr_neutral[2]) > 100) { buf_ = ReadSPI(0x60, 0x20, 10); acc_neutral[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); acc_neutral[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); acc_neutral[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x60, 0x26, 10); acc_sensiti[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); acc_sensiti[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); acc_sensiti[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x60, 0x2C, 10); gyr_neutral[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); gyr_neutral[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); gyr_neutral[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); buf_ = ReadSPI(0x60, 0x32, 10); gyr_sensiti[0] = (Int16)(buf_[0] | ((buf_[1] << 8) & 0xff00)); gyr_sensiti[1] = (Int16)(buf_[2] | ((buf_[3] << 8) & 0xff00)); gyr_sensiti[2] = (Int16)(buf_[4] | ((buf_[5] << 8) & 0xff00)); PrintArray(gyr_neutral, len: 3, d: DebugType.IMU, format: "Factory gyro neutral position: {0:S}"); } HIDapi.hid_set_nonblocking(handle, 1); } private byte[] ReadSPI(byte addr1, byte addr2, uint len, bool print = false) { byte[] buf = { addr2, addr1, 0x00, 0x00, (byte)len }; byte[] read_buf = new byte[len]; byte[] buf_ = new byte[len + 20]; for (int i = 0; i < 100; ++i) { buf_ = Subcommand(0x10, buf, 5, false); if (buf_[15] == addr2 && buf_[16] == addr1) { break; } } Array.Copy(buf_, 20, read_buf, 0, len); if (print) PrintArray(read_buf, DebugType.COMMS, len); return read_buf; } private void PrintArray(T[] arr, DebugType d = DebugType.NONE, uint len = 0, uint start = 0, string format = "{0:S}") { if (d != debug_type && debug_type != DebugType.ALL) return; if (len == 0) len = (uint)arr.Length; string tostr = ""; for (int i = 0; i < len; ++i) { tostr += string.Format((arr[0] is byte) ? "{0:X2} " : ((arr[0] is float) ? "{0:F} " : "{0:D} "), arr[i + start]); } DebugPrint(string.Format(format, tostr), d); } private static float GetNormalizedValue(float value, float rawMin, float rawMax, float normalizedMin, float normalizedMax) { return (value - rawMin) / (rawMax - rawMin) * (normalizedMax - normalizedMin) + normalizedMin; } private static float[] Getn64StickValues(Joycon input) { var isLeft = input.isLeft; var other = input.other; var stick = input.stick; var stick2 = input.stick2; var stick_correction = new float[] { 0f, 0f}; var xAxis = (other == input && !isLeft) ? stick2[0] : stick[0]; var yAxis = (other == input && !isLeft) ? stick2[1] : stick[1]; if (xAxis < input.minX) { input.minX = xAxis; } if (xAxis > input.maxX) { input.maxX = xAxis; } if (yAxis < input.minY) { input.minY = yAxis; } if (yAxis > input.maxY) { input.maxY = yAxis; } var middleX = (input.minX + (input.maxX - input.minX)/2); var middleY = (input.minY + (input.maxY - input.minY)/2); #if DEBUG var desc = ""; desc += "x: "+xAxis+"; y: "+yAxis; desc += "\n X: ["+input.minX+", "+input.maxX+"]; Y: ["+input.minY+", "+input.maxY+"] "; desc += "; middle ["+middleX+", "+middleY+"]"; Debug.WriteLine(desc); #endif var negative_normalized = new float[] {-1, 0}; var positive_normalized = new float[] {0, 1}; var xRange = new float[] {-1f, 1f}; var yRange = new float[] {-1f, 1f}; if (input.realn64Range) { xRange = new float[] {-0.79f, 0.79f}; yRange = new float[] {-0.79f, 0.79f}; } if (xAxis < (middleX - middleX)) { stick_correction[0] = GetNormalizedValue(xAxis, input.minX, (middleX - middleX), xRange[0], 0f); } if (xAxis > (middleX+middleX)) { stick_correction[0] = GetNormalizedValue(xAxis, (middleX+middleX), input.maxX, 0f, xRange[1]); } if (yAxis < (middleY-middleY)) { stick_correction[1] = GetNormalizedValue(yAxis, input.minY, (middleY-middleY), yRange[0], 0f); } if (yAxis > (middleY+middleY)) { stick_correction[1] = GetNormalizedValue(yAxis, (middleY+middleY), input.maxY, 0f, yRange[1]); } return stick_correction; } private static OutputControllerXbox360InputState MapToXbox360Input(Joycon input) { var output = new OutputControllerXbox360InputState(); var swapAB = input.swapAB; var swapXY = input.swapXY; var isPro = input.isPro; var isLeft = input.isLeft; var isSnes = input.isSnes; var is64 = input.is64; var other = input.other; var GyroAnalogSliders = input.GyroAnalogSliders; var buttons = input.buttons; var stick = input.stick; var stick2 = input.stick2; var sliderVal = input.sliderVal; if (is64) { output.axis_right_x = (short) ((buttons[(int)Button.X] ? Int16.MinValue : 0) + (buttons[(int)Button.MINUS] ? Int16.MaxValue : 0)); output.axis_right_y = (short) ((buttons[(int)Button.SHOULDER2_2] ? Int16.MinValue: 0) + (buttons[(int)Button.Y] ? Int16.MaxValue: 0)); var n64Stick = Getn64StickValues(input); output.axis_left_x = CastStickValue(n64Stick[0]); output.axis_left_y = CastStickValue(n64Stick[1]); output.start = buttons[(int)Button.PLUS]; output.a = buttons[(int)(!swapAB ? Button.B : Button.A)]; output.b = buttons[(int)(!swapAB ? Button.A : Button.B)]; output.shoulder_left = buttons[(int)Button.SHOULDER_1]; output.shoulder_right = buttons[(int)Button.SHOULDER2_1]; output.trigger_left = (byte)(buttons[(int)Button.SHOULDER_2] ? Byte.MaxValue : 0); output.trigger_right = (byte)(buttons[(int)Button.STICK] ? Byte.MaxValue : 0); output.dpad_down = buttons[(int)Button.DPAD_DOWN]; output.dpad_left = buttons[(int)Button.DPAD_LEFT]; output.dpad_right = buttons[(int)Button.DPAD_RIGHT]; output.dpad_up = buttons[(int)Button.DPAD_UP]; output.guide = buttons[(int)Button.HOME]; } else if (isPro) { output.a = buttons[(int)(!swapAB ? Button.B : Button.A)]; output.b = buttons[(int)(!swapAB ? Button.A : Button.B)]; output.y = buttons[(int)(!swapXY ? Button.X : Button.Y)]; output.x = buttons[(int)(!swapXY ? Button.Y : Button.X)]; output.dpad_up = buttons[(int)Button.DPAD_UP]; output.dpad_down = buttons[(int)Button.DPAD_DOWN]; output.dpad_left = buttons[(int)Button.DPAD_LEFT]; output.dpad_right = buttons[(int)Button.DPAD_RIGHT]; output.back = buttons[(int)Button.MINUS]; output.start = buttons[(int)Button.PLUS]; output.guide = buttons[(int)Button.HOME]; output.shoulder_left = buttons[(int)Button.SHOULDER_1]; output.shoulder_right = buttons[(int)Button.SHOULDER2_1]; output.thumb_stick_left = buttons[(int)Button.STICK]; output.thumb_stick_right = buttons[(int)Button.STICK2]; } else { if (other != null) { // no need for && other != this output.a = buttons[(int)(!swapAB ? isLeft ? Button.B : Button.DPAD_DOWN : isLeft ? Button.A : Button.DPAD_RIGHT)]; output.b = buttons[(int)(swapAB ? isLeft ? Button.B : Button.DPAD_DOWN : isLeft ? Button.A : Button.DPAD_RIGHT)]; output.y = buttons[(int)(!swapXY ? isLeft ? Button.X : Button.DPAD_UP : isLeft ? Button.Y : Button.DPAD_LEFT)]; output.x = buttons[(int)(swapXY ? isLeft ? Button.X : Button.DPAD_UP : isLeft ? Button.Y : Button.DPAD_LEFT)]; output.dpad_up = buttons[(int)(isLeft ? Button.DPAD_UP : Button.X)]; output.dpad_down = buttons[(int)(isLeft ? Button.DPAD_DOWN : Button.B)]; output.dpad_left = buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.Y)]; output.dpad_right = buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.A)]; output.back = buttons[(int)Button.MINUS]; output.start = buttons[(int)Button.PLUS]; output.guide = buttons[(int)Button.HOME]; output.shoulder_left = buttons[(int)(isLeft ? Button.SHOULDER_1 : Button.SHOULDER2_1)]; output.shoulder_right = buttons[(int)(isLeft ? Button.SHOULDER2_1 : Button.SHOULDER_1)]; output.thumb_stick_left = buttons[(int)(isLeft ? Button.STICK : Button.STICK2)]; output.thumb_stick_right = buttons[(int)(isLeft ? Button.STICK2 : Button.STICK)]; } else { // single joycon mode output.a = buttons[(int)(!swapAB ? isLeft ? Button.DPAD_LEFT : Button.DPAD_RIGHT : isLeft ? Button.DPAD_DOWN : Button.DPAD_UP)]; output.b = buttons[(int)(swapAB ? isLeft ? Button.DPAD_LEFT : Button.DPAD_RIGHT : isLeft ? Button.DPAD_DOWN : Button.DPAD_UP)]; output.y = buttons[(int)(!swapXY ? isLeft ? Button.DPAD_RIGHT : Button.DPAD_LEFT : isLeft ? Button.DPAD_UP : Button.DPAD_DOWN)]; output.x = buttons[(int)(swapXY ? isLeft ? Button.DPAD_RIGHT : Button.DPAD_LEFT : isLeft ? Button.DPAD_UP : Button.DPAD_DOWN)]; output.back = buttons[(int)Button.MINUS] | buttons[(int)Button.HOME]; output.start = buttons[(int)Button.PLUS] | buttons[(int)Button.CAPTURE]; output.shoulder_left = buttons[(int)Button.SL]; output.shoulder_right = buttons[(int)Button.SR]; output.thumb_stick_left = buttons[(int)Button.STICK]; } } // overwrite guide button if it's custom-mapped if (Config.Value("home") != "0") output.guide = false; if (!(isSnes || is64)) { if (other != null || isPro) { // no need for && other != this output.axis_left_x = CastStickValue((other == input && !isLeft) ? stick2[0] : stick[0]); output.axis_left_y = CastStickValue((other == input && !isLeft) ? stick2[1] : stick[1]); output.axis_right_x = CastStickValue((other == input && !isLeft) ? stick[0] : stick2[0]); output.axis_right_y = CastStickValue((other == input && !isLeft) ? stick[1] : stick2[1]); } else { // single joycon mode output.axis_left_y = CastStickValue((isLeft ? 1 : -1) * stick[0]); output.axis_left_x = CastStickValue((isLeft ? -1 : 1) * stick[1]); } } if (!is64) { if (other != null || isPro) { byte lval = GyroAnalogSliders ? sliderVal[0] : Byte.MaxValue; byte rval = GyroAnalogSliders ? sliderVal[1] : Byte.MaxValue; output.trigger_left = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_2 : Button.SHOULDER2_2)] ? lval : 0); output.trigger_right = (byte)(buttons[(int)(isLeft ? Button.SHOULDER2_2 : Button.SHOULDER_2)] ? rval : 0); } else { output.trigger_left = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_2 : Button.SHOULDER_1)] ? Byte.MaxValue : 0); output.trigger_right = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_1 : Button.SHOULDER_2)] ? Byte.MaxValue : 0); } } return output; } public static OutputControllerDualShock4InputState MapToDualShock4Input(Joycon input) { var output = new OutputControllerDualShock4InputState(); var swapAB = input.swapAB; var swapXY = input.swapXY; var isPro = input.isPro; var isLeft = input.isLeft; var isSnes = input.isSnes; var is64 = input.is64; var other = input.other; var GyroAnalogSliders = input.GyroAnalogSliders; var buttons = input.buttons; var stick = input.stick; var stick2 = input.stick2; var sliderVal = input.sliderVal; if (is64) { output.thumb_right_x = (byte) ((buttons[(int)Button.X] ? Byte.MinValue : 0) + (buttons[(int)Button.MINUS] ? Byte.MaxValue : 0)); output.thumb_right_y = (byte) ((buttons[(int)Button.SHOULDER2_2] ? Byte.MinValue: 0) + (buttons[(int)Button.Y] ? Byte.MaxValue: 0)); output.thumb_left_x = CastStickValueByte((other == input && !isLeft) ? -stick2[0] : -stick[0]); output.thumb_left_y = CastStickValueByte((other == input && !isLeft) ? stick2[1] : stick[1]); output.options = buttons[(int)Button.PLUS]; output.cross = buttons[(int)(!swapAB ? Button.B : Button.A)]; output.circle = buttons[(int)(!swapAB ? Button.A : Button.B)]; output.shoulder_left = buttons[(int)Button.SHOULDER_1]; output.shoulder_right = buttons[(int)Button.SHOULDER2_1]; output.trigger_left = buttons[(int)Button.SHOULDER_2]; output.trigger_right = buttons[(int)Button.STICK]; output.trigger_left_value = (byte)(buttons[(int)Button.SHOULDER_2] ? Byte.MaxValue : 0); output.trigger_right_value = (byte)(buttons[(int)Button.STICK] ? Byte.MaxValue : 0); if (buttons[(int)Button.DPAD_UP]) { if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.Northwest; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.Northeast; else output.dPad = DpadDirection.North; } else if (buttons[(int)Button.DPAD_DOWN]) { if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.Southwest; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.Southeast; else output.dPad = DpadDirection.South; } else if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.West; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.East; } if (isPro) { output.cross = buttons[(int)(!swapAB ? Button.B : Button.A)]; output.circle = buttons[(int)(!swapAB ? Button.A : Button.B)]; output.triangle = buttons[(int)(!swapXY ? Button.X : Button.Y)]; output.square = buttons[(int)(!swapXY ? Button.Y : Button.X)]; if (buttons[(int)Button.DPAD_UP]) { if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.Northwest; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.Northeast; else output.dPad = DpadDirection.North; } else if (buttons[(int)Button.DPAD_DOWN]) { if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.Southwest; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.Southeast; else output.dPad = DpadDirection.South; } else if (buttons[(int)Button.DPAD_LEFT]) output.dPad = DpadDirection.West; else if (buttons[(int)Button.DPAD_RIGHT]) output.dPad = DpadDirection.East; output.share = buttons[(int)Button.CAPTURE]; output.options = buttons[(int)Button.PLUS]; output.ps = buttons[(int)Button.HOME]; output.touchpad = buttons[(int)Button.MINUS]; output.shoulder_left = buttons[(int)Button.SHOULDER_1]; output.shoulder_right = buttons[(int)Button.SHOULDER2_1]; output.thumb_left = buttons[(int)Button.STICK]; output.thumb_right = buttons[(int)Button.STICK2]; } else { if (other != null) { // no need for && other != this output.cross = !swapAB ? buttons[(int)(isLeft ? Button.B : Button.DPAD_DOWN)] : buttons[(int)(isLeft ? Button.A : Button.DPAD_RIGHT)]; output.circle = swapAB ? buttons[(int)(isLeft ? Button.B : Button.DPAD_DOWN)] : buttons[(int)(isLeft ? Button.A : Button.DPAD_RIGHT)]; output.triangle = !swapXY ? buttons[(int)(isLeft ? Button.X : Button.DPAD_UP)] : buttons[(int)(isLeft ? Button.Y : Button.DPAD_LEFT)]; output.square = swapXY ? buttons[(int)(isLeft ? Button.X : Button.DPAD_UP)] : buttons[(int)(isLeft ? Button.Y : Button.DPAD_LEFT)]; if (buttons[(int)(isLeft ? Button.DPAD_UP : Button.X)]) if (buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.Y)]) output.dPad = DpadDirection.Northwest; else if (buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.A)]) output.dPad = DpadDirection.Northeast; else output.dPad = DpadDirection.North; else if (buttons[(int)(isLeft ? Button.DPAD_DOWN : Button.B)]) if (buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.Y)]) output.dPad = DpadDirection.Southwest; else if (buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.A)]) output.dPad = DpadDirection.Southeast; else output.dPad = DpadDirection.South; else if (buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.Y)]) output.dPad = DpadDirection.West; else if (buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.A)]) output.dPad = DpadDirection.East; output.share = buttons[(int)Button.CAPTURE]; output.options = buttons[(int)Button.PLUS]; output.ps = buttons[(int)Button.HOME]; output.touchpad = buttons[(int)Button.MINUS]; output.shoulder_left = buttons[(int)(isLeft ? Button.SHOULDER_1 : Button.SHOULDER2_1)]; output.shoulder_right = buttons[(int)(isLeft ? Button.SHOULDER2_1 : Button.SHOULDER_1)]; output.thumb_left = buttons[(int)(isLeft ? Button.STICK : Button.STICK2)]; output.thumb_right = buttons[(int)(isLeft ? Button.STICK2 : Button.STICK)]; } else { // single joycon mode output.cross = !swapAB ? buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.DPAD_RIGHT)] : buttons[(int)(isLeft ? Button.DPAD_DOWN : Button.DPAD_UP)]; output.circle = swapAB ? buttons[(int)(isLeft ? Button.DPAD_LEFT : Button.DPAD_RIGHT)] : buttons[(int)(isLeft ? Button.DPAD_DOWN : Button.DPAD_UP)]; output.triangle = !swapXY ? buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.DPAD_LEFT)] : buttons[(int)(isLeft ? Button.DPAD_UP : Button.DPAD_DOWN)]; output.square = swapXY ? buttons[(int)(isLeft ? Button.DPAD_RIGHT : Button.DPAD_LEFT)] : buttons[(int)(isLeft ? Button.DPAD_UP : Button.DPAD_DOWN)]; output.ps = buttons[(int)Button.MINUS] | buttons[(int)Button.HOME]; output.options = buttons[(int)Button.PLUS] | buttons[(int)Button.CAPTURE]; output.shoulder_left = buttons[(int)Button.SL]; output.shoulder_right = buttons[(int)Button.SR]; output.thumb_left = buttons[(int)Button.STICK]; } } // overwrite guide button if it's custom-mapped if (Config.Value("home") != "0") output.ps = false; if (!(isSnes || is64)) { if (other != null || isPro) { // no need for && other != this output.thumb_left_x = CastStickValueByte((other == input && !isLeft) ? -stick2[0] : -stick[0]); output.thumb_left_y = CastStickValueByte((other == input && !isLeft) ? stick2[1] : stick[1]); output.thumb_right_x = CastStickValueByte((other == input && !isLeft) ? -stick[0] : -stick2[0]); output.thumb_right_y = CastStickValueByte((other == input && !isLeft) ? stick[1] : stick2[1]); } else { // single joycon mode output.thumb_left_y = CastStickValueByte((isLeft ? 1 : -1) * stick[0]); output.thumb_left_x = CastStickValueByte((isLeft ? 1 : -1) * stick[1]); } } if (!is64) { if (other != null || isPro) { byte lval = GyroAnalogSliders ? sliderVal[0] : Byte.MaxValue; byte rval = GyroAnalogSliders ? sliderVal[1] : Byte.MaxValue; output.trigger_left_value = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_2 : Button.SHOULDER2_2)] ? lval : 0); output.trigger_right_value = (byte)(buttons[(int)(isLeft ? Button.SHOULDER2_2 : Button.SHOULDER_2)] ? rval : 0); } else { output.trigger_left_value = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_2 : Button.SHOULDER_1)] ? Byte.MaxValue : 0); output.trigger_right_value = (byte)(buttons[(int)(isLeft ? Button.SHOULDER_1 : Button.SHOULDER_2)] ? Byte.MaxValue : 0); } // Output digital L2 / R2 in addition to analog L2 / R2 output.trigger_left = output.trigger_left_value > 0 ? output.trigger_left = true : output.trigger_left = false; output.trigger_right = output.trigger_right_value > 0 ? output.trigger_right = true : output.trigger_right = false; } return output; } } }