Aiuto sketch per costruire un "knobster" HID.

Cosa è un knobster? Questo:

Sostanzialmente è costituito da 2 rotelline sovrapposte ed un pushbutton in cima; collegato al PC tramite USB viene visto come un joystick / button box; si utilizza con i simulatori di volo ed emula l'analogo comando presente in molti strumenti avionici (GPS, autopilota, selettore frequenze radio, eccetera).

La versione commerciale di questo oggettino viene venduta all'esorbitante cifra di diconsi euro 90 ( :o ); considerato che bastano due encoder rotativi (o un singolo encoder a doppio asse) con pushbutton, pochi grammi di plastica e un Arduino per costruirselo fai-da-te, il vantaggio rispetto alla versione commerciale è innegabile.

Per la parte hardware, avendo già a disposizione due encoder rotativi con pushbutton (questi), ho modificato questo ingegnoso progetto che utilizza due ingranaggi stampati in 3d per trasferire sullo stesso asse il movimento di due encoder rotativi affiancati. Poiché l'autore di questo sistema usa un Raspberry Pi per far funzionare il tutto, sto cercando di scrivere uno sketch per collegarlo all'Arduino Leonardo che già possiedo.

L'obiettivo è fare in modo che il dispositivo sia visto come un button box con 5 pulsanti: rotazione a destra encoder 1, rotazione a sinistra encoder 1, rotazione a destra encoder 2, rotazione a sinistra encoder 2, pushbutton.

Il mio sketch è questo:

#include <Joystick.h>

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID,JOYSTICK_TYPE_GAMEPAD,
  5, 0,                  // Button Count, Hat Switch Count
  false, false, false,   // X, Y and Z Axis
  false, false, false,   // Rx, Ry, Rz
  false, false,          // rudder, throttle
  false, false, false);  // accelerator, brake, steering

const int pinA = 0;
const int pinB = 1;
const int pinC = 2;
const int pinD = 3;
const int puls = 4;

volatile int previous1 = 0;
volatile int counter1 = 0;
volatile int previous2 = 0;
volatile int counter2 = 0;

void setup() {
  pinMode(pinA, INPUT_PULLUP); 
  pinMode(pinB, INPUT_PULLUP);
  pinMode(pinC, INPUT_PULLUP);
  pinMode(pinD, INPUT_PULLUP);
  pinMode(puls, INPUT_PULLUP);

  attachInterrupt(digitalPinToInterrupt(pinA), changed, CHANGE); 
  attachInterrupt(digitalPinToInterrupt(pinB), changed, CHANGE);
  attachInterrupt(digitalPinToInterrupt(pinC), changed, CHANGE);
  attachInterrupt(digitalPinToInterrupt(pinD), changed, CHANGE);
  
  Joystick.begin();
}

int pinToButtonMap = 0;

void changed() {
  int A = digitalRead(pinA); 
  int B = digitalRead(pinB);
  int C = digitalRead(pinC);
  int D = digitalRead(pinD);

  int current1 = (A << 1) | B;
  int combined1  = (previous1 << 2) | current1;
  int current2 = (C << 1) | D;
  int combined2  = (previous2 << 2) | current2;
   
  if(combined1 == 0b0010 || 
     combined1 == 0b1011 ||
     combined1 == 0b1101 || 
     combined1 == 0b0100) {
    counter1++;
  }
   
  if(combined1 == 0b0001 ||
     combined1 == 0b0111 ||
     combined1 == 0b1110 ||
     combined1 == 0b1000) {
    counter1--;
  }

  previous1 = current1;

  if(combined2 == 0b0010 || 
     combined2 == 0b1011 ||
     combined2 == 0b1101 || 
     combined2 == 0b0100) {
    counter2++;
  }
   
  if(combined2 == 0b0001 ||
     combined2 == 0b0111 ||
     combined2 == 0b1110 ||
     combined2 == 0b1000) {
    counter2--;
  }

  previous2 = current2;
}

int lastButtonState = 0;

void loop(){ 
  if(counter1 >= 4) {
    Joystick.setButton(0, 1);
    delay(50);
    Joystick.setButton(0, 0);
    counter1 -= 4;
  } else if(counter1 <= -4) {
    Joystick.setButton(1, 1);
    delay(50);
    Joystick.setButton(1, 0);
    counter1 += 4;
  } 
  if(counter2 >= 4) {
    Joystick.setButton(2, 1);
    delay(50);
    Joystick.setButton(2, 0);
    counter2 -= 4;
  } else if(counter2 <= -4) {
    Joystick.setButton(3, 1);
    delay(50);
    Joystick.setButton(3, 0);
    counter2 += 4;
  }
  
   int currentButtonState = !digitalRead(puls);
  if (currentButtonState != lastButtonState)
  {
    Joystick.setButton(4, currentButtonState);
    lastButtonState = currentButtonState;
  }

  delay(50);
  }

Il pushbutton funziona perfettamente: il pulsante si attiva quando premo il bottone, resta attivo se lo tengo premuto e si disattiva se lo mollo.

Per gli encoder però un comportamento del genere non è adatto, perché vorrei che le due rotelline funzionassero come una rotella del mouse: ad ogni click degli encoder a destra o a sinistra deve corrispondere un click del relativo pulsante.

Con lo sketch qui sopra viene letta bene la direzione degli encoder: si attiva il pulsante giusto a seconda del verso in cui lo ruoto. Inizialmente avevo usato un metodo diverso, senza l'attachinterrupt, in modo da avere più libertà sui PIN da usare, ma la direzione della rotazione non veniva letta in maniera precisa e a volte si attivava il pulsante relativo al verso opposto di rotazione; ho letto che per evitare il problema sarebbe stato necessario collegare l'encoder ad un circuito di debounce usando resistenze e condensatori, che avrebbero reso più complicato il progetto; usando questo sketch invece la direzione della rotazione viene letta sempre in maniera precisa.

Il problema attuale tuttavia è che col mio sketch la corrispondenza tra click dell'encoder e click del pulsante non è precisa, nel senso che a volte alcuni click dell'encoder vanno a vuoto (il pulsante non si attiva) e ciò avviene in maniera totalmente random, senza un rapporto fisso o costante tra click dell'encoder e click del pulsante.

Come potrei modificare il mio sketch in modo da ottenere una corrispondenza 1:1 tra i click dell'encoder e i click del pulsante? E' fattibile via software? Potrebbe altrimenti aiutare quel famoso circuito di debounce con resistenze e condensatori o in questo caso sarebbe ininfluente?

Grazie a chi mi vorrà aiutare.

nessuno0505:
La versione commerciale di questo oggettino viene venduta all'esorbitante cifra di diconsi euro 90 ( :o );

Interessante progettino, magari stasera con calma guardo lo sketch. Quel tipo di encoder so infatti che sono comuni per i simulatori di volo poiché si usano proprio sui cockpit degli aerei (io ho progettato e costruito solo un pannello per un 737NG e FSX che usa solo switch e commutatori) , ma la cosa potrebbe interessarmi perché prima o poi vorrei creare anche quello per l'autopilot...

Detto questo, mi chiedevo dove tu abbia trovato quell'encoder coassiale a ben 80€! Ok che in Italia non so se li vendano (quindi forse è comprensivo di costi di spedizione), ma tra i primi link ho trovato gli ELMA E37 e gli Alps EC11E, e questi ultimi li vendono in USA a poco più di 9$ l'uno (ordine minimo 4). Per cui se in Italia ti hanno chiesto 80€ mi pare eccessivo anche volento considerare il costo dei pomelli (che sono abbastanza particolari), altrimenti credo siano dei ladri...

Scusami, forse mi sono spiegato male io: gli encoder semplici costano pochi euro e l'encoder coassiale immagino costi poco di più ma non l'ho trovato, o meglio: non l'ho nemmeno cercato perché avevo già a disposizione due encoder normali. E' il prodotto finito, questo, che costa 90 euro. In ogni caso se vuoi dare un'occhiata al mio sketch mi fa molto piacere e ti ringrazio. La libreria joystick che utilizzo è quella di MHeironimus, la trovi digitando "arduino joystick library" su Google. Attualmente l'unico problema che mi resta da risolvere è capire se e come è possibile far sì che ad ogni click dell'encoder corrisponda 1 click del relativo "pulsante" che viene visto da windows. Se può essere utile, i miei encoder hanno 24 click e mandano 24 impulsi per 360° di rotazione, quindi ad ogni click dell'encoder corrisponde un impulso.

nessuno0505:
gli encoder semplici costano pochi euro e l'encoder coassiale immagino costi poco di più ma non l'ho trovato, o meglio: non l'ho nemmeno cercato perché avevo già a disposizione due encoder normali.

No, no, l'avevo capito che intendevi parlare di accoppiare due normali encoder, io parlavo proprio del componente hardware coassiale, che non può costare così tanto. Quello del link su Siminnovations costa tanto perché ha aspetto e funzionalità molto simile a quelli dei veri knobster degli aerei, ed ha uscita USB-C.
Quello che serve a te invece è il singolo componente, il cui costo è più che doppio rispetto ai normali encoder rotativi perché ovviamente c'è la difficoltà meccanica di avere due assi uno dentro l'altro. Pensavo lo avessi cercato, comunque se preferisci realizzare il progetto con quello che hai, ok! (anche se non ho capito in che modo hai modificato quel progettino :wink: )

Attualmente l'unico problema che mi resta da risolvere è capire se e come è possibile far sì che ad ogni click dell'encoder corrisponda 1 click del relativo "pulsante" che viene visto da windows.

Per quanto riguarda il tuo sketch, non so ancora darti risposte, ma vedo che gestisci "a manella" l'encoder: inizia ad implementare il singolo encoder usando la libreria Encoder, che sicuramente fai prima e gestisci tutto un po' meglio secondo me, e l'estensione a due encoder (di fatto per Arduino sono due encoder separati) dovrebbe venirti fuori più rapidamente e comodamente...

Grazie della risposta. Certo con un encoder coassiale il progetto verrebbe meglio, il top sarebbe un encoder coassiale con pushbutton ed un Arduino Micro, con i quali verrebbe un oggettino molto simile a quello "originale", ma poiché ho dei componenti rimasti da un precedente progetto open source, che avrei dovuto solo copiare ma che non sono mai riuscito a far funzionare del tutto e che comunque era troppo complesso per le mie capacità da principiante, preferisco recuperare i componenti che ho già in casa, quindi i due encoder "normali" e l'Arduino Leonardo; la modifica che ho fatto consiste semplicemente nel cambiare le misure dei pezzi da stampare in 3d in modo che si adattino allo specifico modello di encoder che ho io.

Quanto allo sketch, ho provato ad usare la libreria Encoder, ma ve ne sono di vario tipo e non saprei quale usare, inoltre fatico a capirne i comandi e di conseguenza non riesco a scrivere nulla di funzionante. Invece con questo sketch qui, basato su un progetto completamente diverso trovato in rete ma che comunque usava un encoder, ho compreso abbastanza chiaramente il significato del codice scritto e quindi sono riuscito a modificarlo capendo cosa stessi facendo: ho raddoppiato l'encoder, ho aggiunto la libreria joystick per gestire l'output ed ho aggiunto la parte per il pushbutton.

In ogni caso manterrò da parte questo sketch e proverò anche a scrivere qualcosa da zero studiando la libreria encoder.

EDIT:

ho provato con la libreria SimpleRotary, l'unica di cui ho capito qualcosa; questo il mio tentativo:

#include <Joystick.h>
#include <SimpleRotary.h>

Joystick_ Joystick(JOYSTICK_DEFAULT_REPORT_ID,JOYSTICK_TYPE_GAMEPAD,
  5, 0,                  // Button Count, Hat Switch Count
  false, false, false,   // X, Y and Z Axis
  false, false, false,   // Rx, Ry, Rz
  false, false,          // rudder, throttle
  false, false, false);  // accelerator, brake, steering

SimpleRotary rotary1(0,1,false);
SimpleRotary rotary2(2,3,false);
const int puls = 4;

void setup() {
  pinMode(puls, INPUT_PULLUP);

  Joystick.begin();
}

int pinToButtonMap = 0;
int lastButtonState = 0;

void loop(){

   byte i;
   
  i = rotary1.rotate();

  if ( i == 1 ) {
    Joystick.setButton(0, 1);
    delay(100);
    Joystick.setButton(0, 0);
  }

  if ( i == 2 ) {
    Joystick.setButton(1, 1);
    delay(100);
    Joystick.setButton(1, 0);

  }

  byte l;

  l = rotary2.rotate();

  if ( l == 1 ) {
    Joystick.setButton(2, 1);
    delay(100);
    Joystick.setButton(2, 0);
  }

  if ( l == 2 ) {
    Joystick.setButton(3, 1);
    delay(100);
    Joystick.setButton(3, 0);

  }
  
   int currentButtonState = !digitalRead(puls);
  if (currentButtonState != lastButtonState)
  {
    Joystick.setButton(4, currentButtonState);
    lastButtonState = currentButtonState;
  }

  delay(50);
  }

Lo sketch è molto più semplice, ma mi dà una lettura assai più imprecisa: a volte mi dà due click del pulsante con un solo click dell'encoder, a volte addirittura legge il verso di rotazione sbagliato, attivando il pulsante "destro" quando giro a sinistra e viceversa. Il mio metodo manuale - non so perché - è più preciso: salta spesso dei click dell'encoder, ma almeno non ho letture doppie o letture al contrario. Sarebbe interessante capire il perché di questo comportamento e se c'è un modo per leggere l'encoder in maniera più precisa.

EDIT2:

Attualmente i miei encoder hanno le due uscite digitali collegate direttamente ai pin di Arduino ed il ground collegato direttamente al ground di Arduino. Potrebbe valere la pena collegare gli encoder con questo circuito?

Visto che i pin di Arduino Leonardo sono già collegati ai 5V tramite le resistenze di pullup, che ho attivato nello sketch, dovrei semplicemente collegare una resistenza ed un condensatore tra le uscite digitali e il ground, è corretto?

C'è possibilità che collegati in questo modo diano delle letture più precise, senza saltare i detent?

Riesumo il mio stesso topic, nel caso possa interessare ad altri: dopo aver acquistato le breadboard, i consensatori e le resistenze, ho provato il circuito qui sopra. Risultato: non cambia assolutamente niente! Collegare semplicemente i due terminali dell'encoder ai pin di Arduino Leonardo, oppure aggiungere il circuito resistenza-condensatore indicato in figura dà esattamente lo stesso tipo di risultato. La soluzione migliore rimane, al momento, usare il circuito più semplice (encoder direttamente su Arduino) con il primo dei due sketch postati. La precisione non è assoluta, nel senso che alcuni click dell'encoder vanno a vuoto per cui non si riesce ad ottenere una corrispondenza perfetta tra click dell'encoder e click del pulsante, ma il verso di rotazione viene sempre letto nel verso giusto, cosa che non accade invece col secondo sketch proposto.