[DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

Vous voulez parler de système d'arcade, de borne d'arcade, de joystick, de hardware console. Vous voulez des infos sur un point technique, c'est ici. 8292
Message
Auteur
Avatar de l’utilisateur
Sebbeug
stick d'argent
Messages : 546
Inscription : 29 nov. 2017, 23:50

[DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#1 Message par Sebbeug »

Il y a quelques mois, j'avais acheté quelques composants pour faire mon propre spinner. J'avais trouvé un tuto sur le net et ça fonctionnait notamment sous Recalbox. J'avais pour idée de me faire un panel spécifique, puis je suis passé à autre chose.
Ce week-end, mon batocera ayant faim de nouveaux ajouts. Je décidais de me ressortir les composants et de finaliser une version qui fonctionnerait sous mon OS du moment et en y ajoutant quelques spec supplémentaires, comme le réglage de la vitesse à la volée. (car oui, entre puchi carat et arkanoid returns par exemple, les sensibilités par défaut ne sont pas les mêmes)

Donc, pour ce tuto (GyuGyu a dû faire une vidéo du même style), on ressort nos composants cheap :

- Arduino Pro Micro à 4 euros sur Aliexpress
- Rotary encoder 600 P/R à 8 euros (600 impulsions par tour)
- 2 boutons Sanwa qui trainaient
- Un petit passage sous mon imprimante 3D pour faire un boitier (source trouvée sur le net et adaptée)

Donc, je suis parti du code Arduino 3 boutons, et j'ai adapté pour simuler une souris 2 boutons, donc un spinner avec 2 boutons seulement.
J'ai ajouté la gestion de la vitesse avec 3 vitesses possibles : 1- Par défaut, 2- Plus lent, 3- Encore plus lent. Lorsqu'on reboote, il retient la dernière position.
Pour changer la vitesse en jeu, rien de plus simple, appui long sur les 3 boutons pendant 2 secondes et la LED d'activité du Pro Micro affiche la position.

Image

Dernière modification par Sebbeug le 13 janv. 2026, 23:47, modifié 1 fois.
WIP de ma TAITO Canary
https://sebbeug.fr/canary

Avatar de l’utilisateur
Sebbeug
stick d'argent
Messages : 546
Inscription : 29 nov. 2017, 23:50

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#2 Message par Sebbeug »

1ère étape : Code Arduino

Installer Arduino IDE, sélectionnez votre Board, et voici le code Arduino à coller :

Code : Tout sélectionner

/*   Arcade Spinner for Batocera
*    
*    by Sebbeug - 2025
*          
*    based on spinner code from Craig B  - (craigbspinner@gmail.com)
*    
*    Features:
*    - 3 sensitivity levels (hold both buttons 2 sec to change)
*    - LED blinks to show current level (1, 2 or 3 blinks)
*    - Settings saved to EEPROM (persists after reboot)
*    
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#include "Mouse.h"
#include <EEPROM.h>

/*   Batocera mod - Arduino IDE -> Tools, Get Board Info
*    VID: 2341 (Arduino)
*    PID: 8037 (Arduino Micro)
*/

// Port Bit/Pin layout   
#define xPD1 0b00000010 //Digital Pin 2  - Micro/PRO Micro - SDA, INT0
#define xPD0 0b00000001 //Digital Pin 3  - Micro/PRO Micro - SCL, INT1
#define xPD_10 (xPD1 | xPD0)
#define xPD4 0b00010000 //Digital Pin 4  - Micro/PRO Micro
#define xPD7 0b10000000 //Digital Pin 6  - Micro/PRO Micro
#define xPE6 0b01000000 //Digital Pin 7  - Micro/PRO Micro

#define pinA 2
#define pinB 3
#define maxBut 3
#define axisFlip 2

// Sensitivity settings
#define EEPROM_ADDR 0
#define LONG_PRESS_MS 2000
#define DEFAULT_SENSITIVITY 1

// Sensitivity levels: 1 = ÷4 (normal), 2 = ÷8 (slow), 3 = ÷16 (very slow)
volatile int sensitivityLevel = DEFAULT_SENSITIVITY;
volatile int rotPositionX = 0;
volatile int rotMultiX = 0;
volatile int prevQuadratureX = 0;

#ifdef axisFlip
volatile int xAxis = 1; 
volatile int yAxis = 0; 
int lastButtonState[maxBut] = {1,1,1};
#else
int lastButtonState[maxBut] = {1,1,0};
#endif

// Long press detection
unsigned long bothButtonsPressed = 0;
bool longPressTriggered = false;

void setup() {
  // Disable TX LED auto-activity (TXLED1 = off on Arduino Micro)
  TXLED1;
  
  // Setup ports
  PORTD = 0b10010011;
  PORTE = 0b01000000;

  // Load sensitivity from EEPROM
  int savedLevel = EEPROM.read(EEPROM_ADDR);
  if (savedLevel >= 1 && savedLevel <= 3) {
    sensitivityLevel = savedLevel;
  } else {
    sensitivityLevel = DEFAULT_SENSITIVITY;
    EEPROM.write(EEPROM_ADDR, sensitivityLevel);
  }

  // Setup interrupts
  attachInterrupt(digitalPinToInterrupt(pinA), pinChangeX, CHANGE); 
  attachInterrupt(digitalPinToInterrupt(pinB), pinChangeX, CHANGE);

  // Start mouse
  Mouse.begin();
  
  // Blink current level at startup
  delay(500);
  blinkLevel(sensitivityLevel);
}

void pinChangeX() {
  int currQuadratureX = PIND & xPD_10;
  int comboQuadratureX = (prevQuadratureX << 2) | currQuadratureX;

  if(comboQuadratureX == 0b0010 || comboQuadratureX == 0b1011 ||
     comboQuadratureX == 0b1101 || comboQuadratureX == 0b0100) {
    rotPositionX--;
  }

  if(comboQuadratureX == 0b0001 || comboQuadratureX == 0b0111 ||
     comboQuadratureX == 0b1110 || comboQuadratureX == 0b1000) {
    rotPositionX++;
  }

  prevQuadratureX = currQuadratureX;
}

void blinkLevel(int level) {
  for (int i = 0; i < level; i++) {
    TXLED0;  // ON
    delay(200);
    TXLED1;  // OFF
    delay(200);
  }
}

void changeSensitivity() {
  sensitivityLevel++;
  if (sensitivityLevel > 3) {
    sensitivityLevel = 1;
  }
  EEPROM.write(EEPROM_ADDR, sensitivityLevel);
  blinkLevel(sensitivityLevel);
}

int getThreshold() {
  switch (sensitivityLevel) {
    case 1: return 4;   // ÷4 (normal)
    case 2: return 8;   // ÷8 (slow)
    case 3: return 16;  // ÷16 (very slow)
    default: return 4;
  }
}

int getShift() {
  switch (sensitivityLevel) {
    case 1: return 2;  // >> 2 = ÷4
    case 2: return 3;  // >> 3 = ÷8
    case 3: return 4;  // >> 4 = ÷16
    default: return 2;
  }
}

void loop() { 
  int currentButtonState;
  int threshold = getThreshold();
  int shift = getShift();

  // Mouse movement with dynamic sensitivity
  if (rotPositionX >= threshold || rotPositionX <= -threshold) {
    rotMultiX = rotPositionX >> shift;
    #ifdef axisFlip
      int x = (xAxis) ? rotMultiX : 0;
      int y = (yAxis) ? rotMultiX : 0;
      Mouse.move(x, y, 0);
    #else 
      Mouse.move(rotMultiX, 0, 0);
    #endif 
    rotPositionX -= (rotMultiX << shift);
  }

  // Read button states
  int btn0 = (PIND & xPD4) >> 4;  // Left button
  int btn1 = (PIND & xPD7) >> 7;  // Right button
  
  // Long press detection (both buttons)
  if (btn0 == 0 && btn1 == 0) {
    if (bothButtonsPressed == 0) {
      bothButtonsPressed = millis();
      longPressTriggered = false;
    } else if (!longPressTriggered && (millis() - bothButtonsPressed >= LONG_PRESS_MS)) {
      changeSensitivity();
      longPressTriggered = true;
    }
  } else {
    bothButtonsPressed = 0;
    longPressTriggered = false;
  }

  // Button handling
  int button = 0;
  do {
    switch (button) {
      case 0:
        currentButtonState = btn0;
        break;
      case 1:
        currentButtonState = btn1;
        break;
      #ifdef axisFlip
      case 2:
        currentButtonState = (PINE & xPE6) >> 6;
        break;
      #endif
      default:
        currentButtonState = 0b00000000;
        break;
    }
    
    if (currentButtonState != lastButtonState[button]) {
      // Don't trigger click if long press was just triggered
      if (!longPressTriggered) {
        switch (button) {
          case 0:
            if (currentButtonState == 1) {
              Mouse.release(MOUSE_LEFT);
            } else {
              Mouse.press(MOUSE_LEFT);
            }        
            break;
          case 1:
            if (currentButtonState == 1) {
              Mouse.release(MOUSE_RIGHT);
            } else {
              Mouse.press(MOUSE_RIGHT);
            }        
            break;
          #ifdef axisFlip
          case 2:
            if (currentButtonState == 0) {
              xAxis = !xAxis;
              yAxis = !yAxis;
            } 
            break;
          #endif
        }
      }
    }

    lastButtonState[button] = currentButtonState;
    ++button;
  } while (button < maxBut);

  // Force TX LED off (USB activity turns it on automatically)
  TXLED1;
}

2ème étape, on soude tout, c'est classique, c'est comme sur tous les autres tutos avec ces composants, rien de nouveau :

ROTARY Encoder -> Pro Micro :
Fil noir -> GND
Fil rouge -> VCC
Fil blanc -> D2
Fil vert -> D3

Bouton Sanwa 1 -> D4
Bouton Sanwa 2 -> D6
Et bien entendu, le GND en chaine
WIP de ma TAITO Canary
https://sebbeug.fr/canary

Avatar de l’utilisateur
Sebbeug
stick d'argent
Messages : 546
Inscription : 29 nov. 2017, 23:50

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#3 Message par Sebbeug »

3ème étape : Impression 3D

Donc pour un boitier 2 boutons :

Image

>> Fichiers STL pour impression
WIP de ma TAITO Canary
https://sebbeug.fr/canary

Avatar de l’utilisateur
Sebbeug
stick d'argent
Messages : 546
Inscription : 29 nov. 2017, 23:50

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#4 Message par Sebbeug »

Finalement, le plus cher dans ce délire, c'est le bouton (knob) en 44x22x6 que j'ai pris en alu, pour une belle finition et un bon grip (et sans inscription)... 10 balles ici
WIP de ma TAITO Canary
https://sebbeug.fr/canary

Avatar de l’utilisateur
Sebbeug
stick d'argent
Messages : 546
Inscription : 29 nov. 2017, 23:50

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#5 Message par Sebbeug »

Oups, j'ai oublié le principal pour Batocera (car par défaut, il n'est pas reconnu), j'ai créé un script qui s'execute au lancement d'un jeu, c'est pas dingue comme méthode, mais ça marche.

A coller ici : /userdata/system/configs/emulationstation/scripts/game-start/

Fichier spinner.sh à coller dans le rep ci-dessus :

Code : Tout sélectionner

#!/bin/bash

# Tuer l'ancien evsieve s'il existe
pkill -f "Arduino Spinner Mouse" 2>/dev/null

# Trouver et configurer l'Arduino
for f in /sys/class/input/event*/device/name; do
    if grep -q "Arduino LLC Arduino Micro" "$f" 2>/dev/null; then
        EVENT=$(echo "$f" | sed 's|.*/\(event[0-9]*\)/.*|\1|')
        evsieve --input /dev/input/$EVENT grab --output name="Arduino Spinner Mouse" &
        break
    fi
done
Enfin, pour Puchi Carat par exemple, lancez le via Mame et dans la config du jeu (sous emulationstation), activez la souris !
Dernière modification par Sebbeug le 14 janv. 2026, 00:51, modifié 2 fois.
WIP de ma TAITO Canary
https://sebbeug.fr/canary

Avatar de l’utilisateur
ZeV
stick d'argent
Messages : 559
Inscription : 28 mars 2018, 14:16
Localisation : Alsace

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#6 Message par ZeV »

Merci pour ce tuto Sebbeug 👍
Image
Sans tabouret, cendrier ni carton SEGA... Mais avec Batocera !!
Et un lave-linge Electrolux 10Kg 1600 trs/min...

Avatar de l’utilisateur
wildpumpk1n
stick de zinc
Messages : 272
Inscription : 21 sept. 2019, 11:03
Localisation : Paris

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#7 Message par wildpumpk1n »

Merci pour le partage :)

Auber1083
stick de zinc
Messages : 351
Inscription : 27 nov. 2016, 13:05
Localisation : 57

Re: [DIY] Spinner pour Batocera (ou Recalbox, Mister ?)

#8 Message par Auber1083 »

Très intéressant ce bricolage et le résultat est très propre.
Ce genre de trucs me permettrait de doser le training sur Arkanoid avec save state.