From b6b7865e133d05c8df1a7ee565094179544aa0b4 Mon Sep 17 00:00:00 2001 From: Youri van der Lans Date: Fri, 21 Nov 2025 01:21:13 +0100 Subject: [PATCH 1/3] Migrate PinSim to Arduino XInput --- pinSim.ino | 163 +++++++++++++++++++++++++++-------------------------- 1 file changed, 84 insertions(+), 79 deletions(-) diff --git a/pinSim.ino b/pinSim.ino index da12fe9..ceea6e3 100644 --- a/pinSim.ino +++ b/pinSim.ino @@ -2,11 +2,12 @@ PinSim Controller v20200517 Controller for PC Pinball games https://www.youtube.com/watch?v=18EcIxywXHg - + Based on the excellent MSF_FightStick XINPUT project by Zack "Reaper" Littell https://github.com/zlittell/MSF-XINPUT - - Uses the Teensy-LC + + Migrated to Arduino XInput Library (https://github.com/dmadison/ArduinoXInput) + Supports Teensy 4.0 and other compatible boards IMPORTANT PLUNGER NOTE: You MUST calibrate the plunger range at least once by holding down "A" @@ -22,7 +23,7 @@ #include #include #include -#include "xinput.h" +#include #include #include @@ -36,7 +37,7 @@ Adafruit_ADXL345_Unified accel = Adafruit_ADXL345_Unified(12345); // configure these boolean flipperL1R1 = true; // boolean fourFlipperButtons = false; // FLIP_L & FLIP_R map to L1/R1 and pins 13 & 14 map to analog L2/R2 100% -boolean doubleContactFlippers = false; // FLIP_L & FLIP_R map to analog L2/R2 10% and pins 13 & 14 map to L2/R2 100% +boolean doubleContactFlippers = false; // FLIP_L & FLIP_R map to analog L2/R2 10% and pins 13 & 14 map to L2/R2 100% boolean analogFlippers = false; // use analog flipper buttons boolean leftStickJoy = false; // joystick moves left analog stick instead of D-pad boolean accelerometerEnabled = true; @@ -70,12 +71,12 @@ float zeroY = 0; #define pinDpadR 1 //Right on DPAD #define pinDpadU 2 //Up on DPAD #define pinDpadD 3 //Down on DPAD -#define pinB1 4 //Button 1 (A) -#define pinB2 5 //Button 2 (B) -#define pinB3 6 //Button 3 (X) -#define pinB4 7 //Button 4 (Y) +#define pinB1 4 //Button 1 (A) +#define pinB2 5 //Button 2 (B) +#define pinB3 6 //Button 3 (X) +#define pinB4 7 //Button 4 (Y) #define pinLB 8 //Button 5 (LB) -#define pinRB 9 //Button 6 (RB) +#define pinRB 9 //Button 6 (RB) #define pinXB 10 //XBOX Guide Button #define pinBK 11 //Button 7 (Back) #define pinST 12 //Button 8 (Start) @@ -153,8 +154,7 @@ Bounce buttonXBOX = Bounce(pinXB, MILLIDEBOUNCE); Bounce button9 = Bounce(pinB9, MILLIDEBOUNCE); Bounce button10 = Bounce(pinB10, MILLIDEBOUNCE); -//Initiate the xinput class and setup the LED pin -XINPUT controller(LED_ENABLED, pinLED1); +// XInput is now a global object, no initialization needed here //void Configure Inputs and Outputs void setupPins() @@ -187,7 +187,7 @@ void setupPins() pinMode(rumbleLarge, OUTPUT); //L3 & R3 pinMode(pinB9, INPUT_PULLUP); - pinMode(pinB10, INPUT_PULLUP); + pinMode(pinB10, INPUT_PULLUP); } //Update the debounced button statuses @@ -221,13 +221,13 @@ void processInputs() { int leftStickX = buttonStatus[POSLT] * -30000 + buttonStatus[POSRT] * 30000; int leftStickY = buttonStatus[POSDN] * -30000 + buttonStatus[POSUP] * 30000; - controller.stickUpdate(STICK_LEFT, leftStickX, leftStickY); - controller.dpadUpdate(0, 0, 0, 0); + XInput.setJoystick(JOY_LEFT, leftStickX, leftStickY); + XInput.setDpad(false, false, false, false); } else { //Update the DPAD - controller.dpadUpdate(buttonStatus[POSUP], buttonStatus[POSDN], buttonStatus[POSLT], buttonStatus[POSRT]); + XInput.setDpad(buttonStatus[POSUP], buttonStatus[POSDN], buttonStatus[POSLT], buttonStatus[POSRT]); } // If Xbox "Back" and joystick Up pressed simultaneously, map joystick to Xbox Left Stick @@ -251,20 +251,20 @@ void processInputs() { flipperL1R1 = false; } - + //Buttons - if (buttonStatus[POSB1]) {controller.buttonUpdate(BUTTON_A, 1);} - else {controller.buttonUpdate(BUTTON_A, 0);} - if (buttonStatus[POSB2]) {controller.buttonUpdate(BUTTON_B, 1);} - else {controller.buttonUpdate(BUTTON_B, 0);} - if (buttonStatus[POSB3]) {controller.buttonUpdate(BUTTON_X, 1);} - else {controller.buttonUpdate(BUTTON_X, 0);} - if (buttonStatus[POSB4]) {controller.buttonUpdate(BUTTON_Y, 1);} - else {controller.buttonUpdate(BUTTON_Y, 0);} - if (buttonStatus[POSB9]) {controller.buttonUpdate(BUTTON_L3, 1);} - else {controller.buttonUpdate(BUTTON_L3, 0);} - if (buttonStatus[POSB10]) {controller.buttonUpdate(BUTTON_R3, 1);} - else {controller.buttonUpdate(BUTTON_R3, 0);} + if (buttonStatus[POSB1]) {XInput.press(BUTTON_A);} + else {XInput.release(BUTTON_A);} + if (buttonStatus[POSB2]) {XInput.press(BUTTON_B);} + else {XInput.release(BUTTON_B);} + if (buttonStatus[POSB3]) {XInput.press(BUTTON_X);} + else {XInput.release(BUTTON_X);} + if (buttonStatus[POSB4]) {XInput.press(BUTTON_Y);} + else {XInput.release(BUTTON_Y);} + if (buttonStatus[POSB9]) {XInput.press(BUTTON_L3);} + else {XInput.release(BUTTON_L3);} + if (buttonStatus[POSB10]) {XInput.press(BUTTON_R3);} + else {XInput.release(BUTTON_R3);} // If BACK and Left Flipper pressed simultaneously, set new plunger dead zone // Compensates for games where the in-game plunger doesn't begin pulling back until @@ -337,15 +337,16 @@ void processInputs() { uint8_t leftTrigger = 0; uint8_t rightTrigger = 0; - if (buttonStatus[POSL1]) {controller.buttonUpdate(BUTTON_LB, 1);} - else {controller.buttonUpdate(BUTTON_LB, 0);} - if (buttonStatus[POSR1]) {controller.buttonUpdate(BUTTON_RB, 1);} - else {controller.buttonUpdate(BUTTON_RB, 0);} + if (buttonStatus[POSL1]) {XInput.press(BUTTON_LB);} + else {XInput.release(BUTTON_LB);} + if (buttonStatus[POSR1]) {XInput.press(BUTTON_RB);} + else {XInput.release(BUTTON_RB);} if (buttonStatus[POSL2]) leftTrigger = 255; else leftTrigger = 0; if (buttonStatus[POSR2]) rightTrigger = 255; else rightTrigger = 0; - controller.triggerUpdate(leftTrigger, rightTrigger); + XInput.setTrigger(TRIGGER_LEFT, leftTrigger); + XInput.setTrigger(TRIGGER_RIGHT, rightTrigger); } // L2/R2 Flippers (standard mode swapped) @@ -353,15 +354,16 @@ void processInputs() { uint8_t leftTrigger = 0; uint8_t rightTrigger = 0; - if (buttonStatus[POSL2]) {controller.buttonUpdate(BUTTON_LB, 1);} - else {controller.buttonUpdate(BUTTON_LB, 0);} - if (buttonStatus[POSR2]) {controller.buttonUpdate(BUTTON_RB, 1);} - else {controller.buttonUpdate(BUTTON_RB, 0);} + if (buttonStatus[POSL2]) {XInput.press(BUTTON_LB);} + else {XInput.release(BUTTON_LB);} + if (buttonStatus[POSR2]) {XInput.press(BUTTON_RB);} + else {XInput.release(BUTTON_RB);} if (buttonStatus[POSL1]) {leftTrigger = 255;} else {leftTrigger = 0;} if (buttonStatus[POSR1]) {rightTrigger = 255;} else {rightTrigger = 0;} - controller.triggerUpdate(leftTrigger, rightTrigger); + XInput.setTrigger(TRIGGER_LEFT, leftTrigger); + XInput.setTrigger(TRIGGER_RIGHT, rightTrigger); } // Double Contact Flippers @@ -375,15 +377,16 @@ void processInputs() if (buttonStatus[POSR1] && buttonStatus[POSR2]) {rightTrigger = 255;} else if (buttonStatus[POSR1] && !buttonStatus[POSR2]) {rightTrigger = 25;} else if (!buttonStatus[POSR1] && !buttonStatus[POSR2]) {rightTrigger = 0;} - controller.triggerUpdate(leftTrigger, rightTrigger); + XInput.setTrigger(TRIGGER_LEFT, leftTrigger); + XInput.setTrigger(TRIGGER_RIGHT, rightTrigger); } //Middle Buttons - if (buttonStatus[POSST]&&buttonStatus[POSBK]){controller.buttonUpdate(BUTTON_LOGO, 1);} - else if (buttonStatus[POSST]){controller.buttonUpdate(BUTTON_START, 1);} - else if (buttonStatus[POSBK]){controller.buttonUpdate(BUTTON_BACK, 1);} - else if (buttonStatus[POSXB]){controller.buttonUpdate(BUTTON_LOGO, 1);} - else {controller.buttonUpdate(BUTTON_LOGO, 0); controller.buttonUpdate(BUTTON_START, 0); controller.buttonUpdate(BUTTON_BACK, 0);} + if (buttonStatus[POSST]&&buttonStatus[POSBK]){XInput.press(BUTTON_LOGO); XInput.release(BUTTON_START); XInput.release(BUTTON_BACK);} + else if (buttonStatus[POSST]){XInput.press(BUTTON_START); XInput.release(BUTTON_LOGO); XInput.release(BUTTON_BACK);} + else if (buttonStatus[POSBK]){XInput.press(BUTTON_BACK); XInput.release(BUTTON_LOGO); XInput.release(BUTTON_START);} + else if (buttonStatus[POSXB]){XInput.press(BUTTON_LOGO); XInput.release(BUTTON_START); XInput.release(BUTTON_BACK);} + else {XInput.release(BUTTON_LOGO); XInput.release(BUTTON_START); XInput.release(BUTTON_BACK);} //Experimental Analog Input //Analog flippers @@ -391,14 +394,15 @@ void processInputs() { uint8_t leftTrigger = map(analogRead(pinLT), 0, 512, 0, 255); uint8_t rightTrigger = map(analogRead(pinRT), 0, 512, 0, 255); - controller.triggerUpdate(leftTrigger, rightTrigger); + XInput.setTrigger(TRIGGER_LEFT, leftTrigger); + XInput.setTrigger(TRIGGER_RIGHT, rightTrigger); } - + //Tilt if (accelerometerEnabled && !leftStickJoy) { - /* Get a new sensor event */ - sensors_event_t event; + /* Get a new sensor event */ + sensors_event_t event; accel.getEvent(&event); // Zero accelerometer when START is first pressed (PinSim Yellow Start Button) @@ -421,7 +425,7 @@ void processInputs() int leftStickY = zeroY + (event.acceleration.y * nudgeMultiplier); if (millis() > tiltEnableTime) { - controller.stickUpdate(STICK_LEFT, leftStickX, leftStickY); + XInput.setJoystick(JOY_LEFT, leftStickX, leftStickY); } } @@ -430,7 +434,7 @@ void processInputs() if (plungerEnabled) { int reading = analogRead(pinPlunger); - + if ((reading - lastReading) > -10 && (reading - lastReading) < 10 || (reading - lastReading > 75) || (reading - lastReading < -75)) { ave.push(reading); @@ -460,7 +464,7 @@ void processInputs() if (currentDistance - lastDistance >= adjustedPlungeTrigger) { // we throw STICK_RIGHT to 0 to better simulate the physical behavior of a real analog stick - controller.stickUpdate(STICK_RIGHT, 0, 0); + XInput.setJoystick(JOY_RIGHT, 0, 0); // disable plunger momentarily to compensate for spring bounce plungerReportTime = millis() + 1000; distanceBuffer = plungerMaxDistance; @@ -482,11 +486,11 @@ void processInputs() else if (currentDistance <= plungerMinDistance + 50) { currentlyPlunging = true; - controller.stickUpdate(STICK_RIGHT, 0, -32760); + XInput.setJoystick(JOY_RIGHT, 0, -32760); distanceBuffer = plungerMinDistance; tiltEnableTime = millis() + 1000; } - + // cap min else if (currentDistance > plungerMaxDistance) { @@ -499,14 +503,16 @@ void processInputs() if (currentlyPlunging) { - controller.stickUpdate(STICK_RIGHT, 0, map(distanceBuffer, plungerMaxDistance, plungerMinDistance, zeroValue, -32760)); + XInput.setJoystick(JOY_RIGHT, 0, map(distanceBuffer, plungerMaxDistance, plungerMinDistance, zeroValue, -32760)); } - else controller.stickUpdate(STICK_RIGHT, 0, map(distanceBuffer, plungerMaxDistance, plungerMinDistance, 0, -32760)); + else XInput.setJoystick(JOY_RIGHT, 0, map(distanceBuffer, plungerMaxDistance, plungerMinDistance, 0, -32760)); } // Rumble - analogWrite(rumbleSmall, controller.rumbleValues[1]); - analogWrite(rumbleLarge, controller.rumbleValues[0]); + // Receive rumble data from XInput + XInput.receive(); + analogWrite(rumbleSmall, XInput.getRumbleRight()); + analogWrite(rumbleLarge, XInput.getRumbleLeft()); // Duplicate rumble signals on both motors (causes unacceptable current draw) // if (controller.rumbleValues[0] > 0 && controller.rumbleValues[1] == 0x00) @@ -517,7 +523,7 @@ void processInputs() // { // analogWrite(rumbleLarge, controller.rumbleValues[1]); // } - + } uint16_t readingToDistance(int16_t reading) @@ -533,7 +539,7 @@ uint16_t getPlungerAverage() for (int i=0; i -10 && (reading - lastReading) < 10) { ave.push(reading); @@ -587,7 +593,7 @@ void deadZoneCompensation() flashStartButton(); buttonUpdate(); // ensure just one calibration per button press - while (digitalRead(POSBK) == LOW) + while (digitalRead(pinBK) == LOW) { // wait... } @@ -605,11 +611,16 @@ void flashStartButton() } //Setup -void setup() +void setup() { + // Initialize XInput library + XInput.begin(); + // Enable auto-send so we don't need to call send() manually (but we'll keep it for explicit control) + XInput.setAutoSend(false); + setupPins(); delay(500); - + // rumble test (hold Left Flipper on boot) if (digitalRead(pinLB) == LOW) { @@ -623,7 +634,7 @@ void setup() analogWrite(rumbleSmall, str); delay(10); } - + for (int str=0; str < 256; str++) { analogWrite(rumbleLarge, str); @@ -653,7 +664,7 @@ void setup() flashStartButton(); } } - + if (accelerometerEnabled) { /* Set the range to whatever is appropriate for your project */ @@ -661,9 +672,9 @@ void setup() // accel.setRange(ADXL345_RANGE_8_G); // accel.setRange(ADXL345_RANGE_4_G); accel.setRange(ADXL345_RANGE_2_G); - + delay(2500); // time to lower the cabinet lid - sensors_event_t event; + sensors_event_t event; accel.getEvent(&event); zeroX = event.acceleration.x * nudgeMultiplier * -1; zeroY = event.acceleration.y * nudgeMultiplier * -1; @@ -672,7 +683,7 @@ void setup() // plunger setup plungerMin = getPlungerAverage(); - if (plungerEnabled) + if (plungerEnabled) { plungerMax = EEPROM.readInt(0); plungerMin = EEPROM.readInt(10); @@ -692,20 +703,14 @@ void setup() } } -void loop() +void loop() { //Poll Buttons buttonUpdate(); - + //Process all inputs and load up the usbData registers correctly processInputs(); - //Update the LED display - controller.LEDUpdate(); - - //Send data - controller.sendXinput(); - - //Receive data - controller.receiveXinput(); -} + //Send data (rumble receive is handled in processInputs) + XInput.send(); +} From e4dd5e4de2d9be8ec3d89f5d06e80a501f891b6b Mon Sep 17 00:00:00 2001 From: Youri van der Lans Date: Fri, 21 Nov 2025 14:00:39 +0100 Subject: [PATCH 2/3] Updarted readme and comments to reflect migration --- README.md | 18 +++++++++++++++--- pinSim.ino | 11 +++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 51ced7d..36e13eb 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,32 @@ # PinSim XInput Game Controller for PC Pinball Games -Based on the excellent [MSF_FightStick XINPUT](https://github.com/zlittell/MSF-XINPUT) project by Zack "Reaper" Littell +Based on the excellent [Arduino XInput Library](https://github.com/dmadison/ArduinoXInput) library by David Madison. #### The complete project build guide, along with PCB links, wiring schematics, and 3D STL files, is [hosted here](https://github.com/Jerware/PinSim/blob/master/pinsim_instructions.pdf) and backed up on [Archive.org](https://web.archive.org/web/20180209074729/https://www.tested.com/tech/gaming/569647-how-build-pinsim-virtual-reality-pinball-machine/). This code is designed to run on Teensy LC and requires the following libraries: -- [MSF-XInput](https://github.com/zlittell/MSF-XINPUT/tree/master/MSF_XINPUT) +- [Arduino XInput](https://github.com/dmadison/ArduinoXInput) - [Adafruit Sensor](https://github.com/adafruit/Adafruit_Sensor) - [Adafruit ADXL345](https://github.com/adafruit/Adafruit_ADXL345) - [Bounce](https://www.pjrc.com/teensy/td_libs_Bounce.html) - [Average](https://github.com/MajenkoLibraries/Average) - [EEPROMex](https://github.com/thijse/Arduino-EEPROMEx) -Please note the instructions for the MSF-XInput library, which requires some Teensyduino files to be overwritten in order to add support for a new USB device type. +Please note the instructions for the Arduino XInput library, which requires some Teensyduino files to be overwritten in order to add support for a new USB device type. I have included compiled code that can be installed on a Teensy LC using the [Teensy Loader](https://www.pjrc.com/teensy/loader.html) application. The code will compile for Teensy 3.2 but, besides being more expensive, that board lacks sufficient current output on the LED pins. +#### Setup instructions: + +1. Install "XInput" library via Arduino Library Manager (by David Madison) +2. Download ArduinoXInput_Teensy from: https://github.com/dmadison/ArduinoXInput_Teensy +3. Follow installation instructions to copy files to Teensy hardware folder +4. Restart Arduino IDE +5. Select Tools > Board > Teensy 4.x +6. Select Tools > USB Type > XInput + #### OPTIONS OVERVIEW: * BACK + LEFT FLIPPER: Home analog plunger (if installed) @@ -29,6 +38,9 @@ I have included compiled code that can be installed on a Teensy LC using the [Te Note: If installed, you MUST calibrate the analog plunger range at least once by holding down "A" when plugging in the USB cable. LED-1 should flash rapidly, and then you should pull the plunger all the way out and release it all the way back in. The LED1 should flash again, and normal operation resumes. The setting is saved between power cycles. +#### Changelog 21/11/2025: +* Migrated to Arduino XInput Library + #### New features added 05/17/2020: * GPIO 21 & 23 connected to L3 & R3 (analog stick buttons). These GPIO pins were previously unused. diff --git a/pinSim.ino b/pinSim.ino index ceea6e3..aefc343 100644 --- a/pinSim.ino +++ b/pinSim.ino @@ -1,13 +1,10 @@ /* - PinSim Controller v20200517 + PinSim Controller v20251121 Controller for PC Pinball games https://www.youtube.com/watch?v=18EcIxywXHg - Based on the excellent MSF_FightStick XINPUT project by Zack "Reaper" Littell - https://github.com/zlittell/MSF-XINPUT - - Migrated to Arduino XInput Library (https://github.com/dmadison/ArduinoXInput) - Supports Teensy 4.0 and other compatible boards + Based on the Arduino XInput Library by David Madison + https://github.com/dmadison/ArduinoXInput IMPORTANT PLUNGER NOTE: You MUST calibrate the plunger range at least once by holding down "A" @@ -614,6 +611,8 @@ void flashStartButton() void setup() { // Initialize XInput library + // Note: For Teensy with XInput USB mode, ensure ArduinoXInput_Teensy package is installed + // If you get multiple definition errors, the core and library are conflicting XInput.begin(); // Enable auto-send so we don't need to call send() manually (but we'll keep it for explicit control) XInput.setAutoSend(false); From fba605812fe1ed902c9c1530db842513feb05bec Mon Sep 17 00:00:00 2001 From: Youri van der Lans Date: Fri, 21 Nov 2025 14:06:34 +0100 Subject: [PATCH 3/3] Updated readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 36e13eb..aa89758 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ I have included compiled code that can be installed on a Teensy LC using the [Te #### Setup instructions: -1. Install "XInput" library via Arduino Library Manager (by David Madison) +1. Install all required libraries via Arduino Library Manager 2. Download ArduinoXInput_Teensy from: https://github.com/dmadison/ArduinoXInput_Teensy 3. Follow installation instructions to copy files to Teensy hardware folder 4. Restart Arduino IDE