9 marzo 2019

Costruisco un GAMEPAD con Raspberry Pi #02 - Stick!


Benvenuti nel secondo episodio della serie #PiPAD: costruisco un GAMEPAD con Raspberry Pi!

Dopo aver iniziato a collegare i primi sensori al nostro controller (rimando alla prima puntata per maggiori dettagli), ora si passa ad un componente più complesso: lo Stick Analogico! Come sempre il tutto è gestito via codice Python su un Raspberry Pi Zero (wireless).

Prima di entrare nel merito, vi riporto il video abbinato a questo articolo che ho pubblicato sul mio canale YouTube nel quale viene mostrato il risultato in esecuzione e spiegato a grandi linee il funzionamento di questo interessante sensore:


Il secondo step di questa nuova avventura è quello di aggiungere questo componente analogico che necessita di alcuni accorgimenti particolari per poter dialogare con il Raspberry Pi (che ragiona solo in digitale).


Componenti hardware necessarie


Per aggiungere lo Stick al nostro GAMEPAD occorreranno:
  • 1 Stick Analogico
  • 1 converter da Analogico a Digitale, come l'ADS1115
  • 1 kit per saldare visto che molto probabilmente il convertitore avrà la testata scollegata dal chip stesso
  • cavi a sufficienza per tutte le connessioni da Stick a Converter e breadboard, e da Converter e breadboard al Pi
Le componenti utilizzate in questo secondo step sono reperibili con facilità online, e sono relativamente economiche. Le parti più costose saranno il converter e, se non se ne dispone già di uno, il kit per saldare.



Uno Stick è un sensore che permette di imputare due valori che identificano la posizione di un cursore su un immaginario piano cartesiano. Lo stesso cursore può essere premuto, trasformando il grosso stick in un bottone. Un sensore di questo tipo ha 5 PIN: un PIN deve essere collegato al GND, un PIN ai 5V, un PIN mappa lo stato del bottone e può lavorare in digitale (0 o 1, cioè rilasciato o premuto) e può essere collegato direttamente ad un PIN del Pi (nel codice demo è stato scelto il PIN 36), ed infine due PIN che lavorano in analogico e servono per conoscere la posizione del cursore sull'asse X e sull'asse Y.


Per far dialogare i PIN analogici dello Stick con quelli del Raspberry Pi (che sono solamente digitali) occorre interpellare un terzo attore che faccia da "passacarte" tra i due: il convertitore ADS1115, o un qualsiasi altro ADC da almeno 10Bit (il modello 1115 è a 16Bit).
Nota: in questa guida si terrà in considerazione il suddetto modello, ma chiaramente può essere sostituito da qualsiasi altro chip modificando il codice nei punti chiave.
Nota: un convertitore da analogico a digitale consente di tradurre i segnali provenienti da un sensore che lavora in analogico affinché essi siano "digeribili" dal Raspberry Pi: collegando direttamente un sensore analogico al Pi, infatti, accadrebbe che quest'ultimo non sarebbe in grado di differenziare tutti i valori possibili ma soltanto di capire quando il sensore non è in uso (False) o quando lo è (True).

L'ADS1115 espone fino a 4 canali in grado di trasformare gli stati dei sensori analogici ad esso connessi in numeri interi facilmente utilizzabili sul Pi. Il sensore ha 10 PIN dei quali, per i nostri scopi, ne bastano 6: un PIN va collegato al GND, un PIN ai 3.3V, due PIN (es. A0 ed A1) vanno collegati ai rispettivi PIN analogici X ed Y dello stick (nell'esempio si è usato il canale 0 per Y e l'1 per X), ed infine altri due PIN servono per dialogare col Raspberry Pi. Questi ultimi due sono il PIN SCL ed il PIN SDA che vanno abbinati ai corrispondenti SCL ed SDA (cioè i PIN 3 e 5) della testata GPIO del Pi. Questi due PIN sono particolari perché assieme danno vita all'interfaccia I2C: l'SDA serve per i dati, metre l'SCL per il clock.
Nota: come anticipato, è molto probabile che l'ADS1115 (o equivalente) arrivi con la propria testata separata dal chip, e quindi sarà nostro compito saldare il tutto affinché il convertitore possa essere correttamente riconosciuto dal Raspberry Pi.

Al fine di utilizzare il convertitore (che come detto sfrutterà l'I2C per dialogare con il Pi), sarà indispensabile abilitare l'interfaccia I2C sul Raspberry Pi. Per fare ciò, digitare il comando sudo raspi-config quindi entrare nel menu Interfacing Options > I2C e selezionare Enable. Una volta completata questa operazione si potrà verificare se l'ADS1115 sarà stato correttamente collegato tramite il tool i2cdetect. Questo tool dovrebbe essere preinstallato nel sistema operativo, qualora non lo fosse basterà un sudo apt install i2c-tools per prelevarlo.


A questo punto, digitando i2cdetect -y 1 apparirà una mappa che mostrerà tutti i device connessi agli indirizzi possibili. La mappa non dovrà essere vuota: se così fosse significherà che il convertitore non sarà stato riconosciuto e ciò potrà dipendere da tanti fattori, tra cui il mal funzionamento del chip stesso o qualche connessione errata (e via discorrendo).
Nota: è fondamentale che appaia un valore nella mappa, altrimenti sarà impossibile far dialogare Stick e Raspberry Pi.


La libreria Adafruit e la posizione/direzione del cursore


Una volta connessi Stick e ADS1115 a breadboard e Raspberry Pi, per poter utilizzare il convertitore occorrerà installare una apposita libreria sviluppata da Adafruit. Questa libreria si occuperà del "lavoro sporco", cioè di interpretare i segnali analogici e restituire un numero intero corrispondente ad ogni posizione occupata dal cursore, esponendo dei semplici metodi di interrogazione.

La libreria può essere installata tramite pip che, qualora non lo aveste già sul vostro sistema, potrà essere facilmente reperito tramite il comando sudo apt install python3-pip ed automaticamente configurato. A questo punto con sudo pip3 install adafruit-circuitpython-ads1x15 si potrà prelevare la libreria ed il sistema si occuperà di completare tutti i passaggi necessari alla sua installazione.
ATTENZIONE: la libreria lavora utilizzando lo standard BCM per dialogare con il GPIO del Raspberry Pi. Questo significa che in qualsiasi altro punto del codice in cui si fa accesso alla testata si dovrà utilizzare lo stesso standard. Nella precedente puntata si utilizzava lo standard BOARD per far dialogare sensori e Raspberry Pi: andrà modificato il codice in tutti i punti affinché la modalità d'uso sia cambiata con quella BCM, e con essa la numerazione dei PIN.

Come detto, la libreria ci consentirà di catturare i segnali dello Stick. Ma come fare per capire in quale posizione si trovi il cursore? Tanto per cominciare possiamo accontentarci della direzione verso cui esso sia rivolto: piuttosto che cercare di interpretare la posizione precisa sul piano cartesiano immaginario in cui la levetta è posta, può essere più che sufficiente identificare la direzione verso cui essa sia diretta.


Utilizzando la rosa dei venti è possibile immaginare fino ad 8 direzioni in cui il cursore può spostarsi. A tali direzioni, che in ordine antiorario partendo da Nord possiamo rinominare in 1, 2, 3... 8 (con 0 la posizione di stop al centro), dovranno corrispondere sempre due coordinate per X ed Y.
Ciò che è facilmente intuibile è che considerando i 4 punti cardinali (N, S, W, E) per le tre possibili direzioni di ognuno di essi (es: NW, N ed NE) uno dei due valori tra X ed Y sarà fisso mentre l'altro varierà. Gli stati possibili per ogni coordinata sono 3 e corrispondono a un valore variabile al di sotto di una soglia minima, un valore variabile al di sopra di una soglia massima, ed un valore variabile intermedio tra minimo e massimo. Con 2 coordinate e 3 possibili stati per ognuna (2³) si coprono proprio le 8 direzioni.

Nell'esempio, i valori minino e massimo calcolati sono 1200 e 1300, per cui X ed Y saranno sempre o minori di 1200, o maggiori di 1300 o compresi tra i due. E di seguito sono riportate tutte le possibili combinazioni:
  1. N
    • X < 1200
    • 1200 < Y < 1300
  2. NW
    • X < 1200
    • Y > 1300
  3. W
    • 1200 < X < 1300
    • Y > 1300
  4. SW
    • X > 1300
    • Y > 1300
  5. S
    • X > 1300
    • 1200 < Y < 1300
  6. SE
    • X > 1300
    • Y < 1200
  7. E
    • 1200 < X < 1300
    • Y < 1200
  8. NE
    • X < 1200
    • Y < 1200
  • Fermo
    • 1200 < X < 1300
    • 1200 < Y < 1300
Nota: i valori sono quelli rilevati con l'ADS1115 e le direzioni hanno "senso" se lo Stick viene montato nello stesso verso in cui è stato montato nell'esempio.


Il software


A questo punto tutte le componenti (hardware e software) dovrebbero essere pronte: Stick e converter correttamente connessi, l'i2c abilitato e la libreria Adafruit installata. Inoltre sappiamo come sfruttare il sensore, quindi si può passare al codice di controllo.

Come per tutti gli altri sensori, anche per dialogare con lo Stick si creerà una classe specifica (PiPadStick.py) e si inseriranno i valori di configurazione nell'apposito file config.py condiviso. La seguente classe consente di controllare lo Stick e può sia essere richiamata da applicazioni esterne (come quella principale), sia direttamente per test:

# GPIO
import RPi.GPIO as _g
# time utilities
import time as _t
# multiprocessing
import multiprocessing as _mp
#import adc
import board
import busio
import adafruit_ads1x15.ads1015 as ADS
from adafruit_ads1x15.analog_in import AnalogIn

 # configuration
from config import stick_conf as _conf

 # PIPAD BUTTONS
class PiPadStick:

     # initialization
    def __init__(self, led) :

         # set PINs on BOARD
        if _conf['DEVEL_LOG'] :
            print("Initializing Stick...")
            print("> button", _conf['btn_pin'])
        _g.setmode(_g.BCM)
        _g.setup(_conf['btn_pin'], _g.IN, pull_up_down=_g.PUD_UP)

         # initialize LED
        self.led = led

         # initialize ADC
        i2c = busio.I2C(board.SCL, board.SDA)
        self.ads = ADS.ADS1015(i2c)

         # values
        self.btn = _mp.Value('i', 1)
        self.x = _mp.Value('i', 0)
        self.y = _mp.Value('i', 0)

         # processes
        if _conf['DEVEL_LOG'] : print("Initializing processes...")
        self.process1 = _mp.Process(target=self.B)
        self.process2 = _mp.Process(target=self.worker)
        self.process1.start()
        self.process2.start()

         if _conf['DEVEL_LOG'] : print("...init done!")

     # terminate
    def terminate(self) :
        if _conf['DEVEL_LOG'] : print("Stick termination...")
        self.process1.terminate()
        self.process2.terminate()

     def isButtonPressed(self) :
        return self.btn.value == False

     def xAxisValue(self) :
        return self.x.value

     def yAxisValue(self) :
        return self.y.value

     # vedere mappatura "rosa dei venti" per gli stati
     def position(self) :
        min = _conf['min']
        max = _conf['max']
        x = self.xAxisValue()
        y = self.yAxisValue()

         if (x > min and x < max and y > min and y < max) :
            if _conf['DEVEL_LOG'] : print("fermo")
            return 0
        elif (x > min and x < max) :
            #if _conf['DEVEL_LOG'] : print("asse x")
            #if _conf['DEVEL_LOG'] : print("> x: ", x)
            #if _conf['DEVEL_LOG'] : print("> y: ", y)
            if (y < min) :
                if _conf['DEVEL_LOG'] : print("dx")
                return 7
            elif (y > max) :
                if _conf['DEVEL_LOG'] : print("sx")
                return 3
        elif (x < min) :
            #if _conf['DEVEL_LOG'] : print("sopra")
            #if _conf['DEVEL_LOG'] : print("> x: ", x)
            #if _conf['DEVEL_LOG'] : print("> y: ", y)
            if (y > min and y < max) :
                if _conf['DEVEL_LOG'] : print("su")
                return 1
            elif (y < min) :
                if _conf['DEVEL_LOG'] : print("su dx")
                return 8
            elif (y > max) :
                if _conf['DEVEL_LOG'] : print("su sx")
                return 2
        elif (x > max) :
            #if _conf['DEVEL_LOG'] : print("sotto")
            #if _conf['DEVEL_LOG'] : print("> x: ", x)
            #if _conf['DEVEL_LOG'] : print("> y: ", y)
            if (y > min and y < max) :
                if _conf['DEVEL_LOG'] : print("giu")
                return 5
            elif (y < min) :
                if _conf['DEVEL_LOG'] : print("giu dx")
                return 6
            elif (y > max) :
                if _conf['DEVEL_LOG'] : print("giu sx")
                return 4

     # control button
    def B(self) :
        while True :
            self.btn.value = _g.input(_conf['btn_pin'])
            if not self.led == False :
                if btn.value == False :
                    self.led.yellowOn()
                else :
                    self.led.yellowOff()
            _t.sleep(_conf['btn_wait_time'])

     # control axis
    def worker(self) :
        while True :
            val_x = AnalogIn(self.ads, ADS.P1)
            val_y = AnalogIn(self.ads, ADS.P0)
            self.x.value = val_x.value
            self.y.value = val_y.value
            _t.sleep(_conf['axis_wait_time'])

 # DEBUG
if __name__ == "__main__":
    print ("Welcome! Testing Axis:")
    stick = PiPadStick(False)
    try:
        print("Stick positions")
        print("N: 1, NW: 2, W: 3, SW: 4, S: 5, SE: 6, E: 7, NE: 8")
        while True:
            if (stick.isButtonPressed() == True) :
                print("Stick BTN pressed!")
            position = stick.position()
            if (position is not 0) :
                print("Stick position: ", position)
            _t.sleep(_conf['btn_wait_time'])
    except KeyboardInterrupt:
        pass
    print ("Goodbye!")
    stick.terminate()
    _g.cleanup()
Nota: il codice di test resterà in attesa della pressione dello stick oppure del suo movimento verso una delle 8 direzioni possibili mostrando a video un messaggio relativo all'azione dell'utente. Si utilizzano due processi, uno per gestire la pressione e l'altro per calcolare la direzione dello Stick, ed in più (se viene passato il gestore LED come parametro in fase di instaziamento della classe) il LED verrà colorato di colore giallo alla pressione della levetta.

All'interno del codice si fa riferimento alla configurazione, essa è inserita all'interno di un apposito file esterno e contiene i parametri generali:

[...] # altri valori nella configurazione

stick_conf = {
    'DEVEL_LOG' : False,
    'btn_wait_time' : 0.1,
    'axis_wait_time' : 0.1,
    'btn_pin' : 16, #36
    'x_pin' : 1,
    'y_pin' : 0,
    'min' : 1200,
    'max' : 1300
}

Infine, l'applicazione principale non farà altro che integrare la classe appena descritta per usarla (assieme alle altre già presenti) all'interno di un ciclo infinito in cui si rimane in attesa dell'input da parte dell'utente:

# GPIO
import RPi.GPIO as _g
import time as _t

# configuration
from config import configuration as _conf
# buttons
from PiPadButton import PiPadButton
# leds
from PiPadLED import PiPadLED
# sticks
from PiPadStick import PiPadStick

if _conf['DEVEL_LOG'] : print("Starting...")
led = PiPadLED()
button = PiPadButton(led)
stick = PiPadStick(led)
if _conf['DEVEL_LOG'] : print("Started!")

try:
    if _conf['DEVEL_LOG'] : print("Waiting for input!")
    while True:
        if (button.isButton1Pressed() == True) :
            if _conf['DEVEL_LOG'] : print("BTN1 pressed!")

        if (button.isButton2Pressed() == True) :
            if _conf['DEVEL_LOG'] : print("BTN2 pressed!")

        if (stick.isButtonPressed() == True) :
            if _conf['DEVEL_LOG'] : print("BTN Stick pressed!")

        position = stick.position() 
        if (position is not 0) :
            if _conf['DEVEL_LOG'] : print("Stick position: ", position)

        _t.sleep(0.2)

# capture interruption
except KeyboardInterrupt:
    pass

# exit cycle
finally:
    if _conf['DEVEL_LOG'] : print("Exiting!")
    button.terminate()
    stick.terminate()

# clean
_g.cleanup()
Nota: al momento il codice non fa molto altro se non verificare la pressione dello Stick o la sua direzione (in grassetto le aggiunte rispetto alla prima puntata), oltre che continuare a gestire i bottoni ed il LED RGB di feedback.


Conclusioni


Bene, termina qui il secondo episodio della serie!

Nelle prossime puntate, tra gli altri, aggiungeremo un rotary encoder al nostro GAMEPAD cosicché possa essere sfruttato per avere altri metodi di input.


A presto :-)

Nessun commento:

Posta un commento