venerdì 25 marzo 2022

Applicazione accesso a parcheggio - Entry ed Exit state

Automazione parking access

Come dice una famosa canzone: “solo il pretesto per fare una canzone”, io vi dico che l’applicazione qui descritta è solo il pretesto per fare un programma. In effetti è proprio così, non ho particolare interesse per il parking access, ma dovevo scegliere una applicazione pratica che si potesse sviluppare con il simulatore online wokwi. L’attrazione principale del programma è la macchina a stati finiti basata sullo switch case. Nel precedente articolo mi ero riproposto di farcire i case di codice implementando nuove caratteristiche, ad esempio implementare il lampeggiante, oppure usare il display. Però avevo anche parlato di implementare gli entry state ed exit state ed è bene che lo faccia prima di farcire i case di codice. Allora vediamo cosa sono questi entry state o OnEntryState ed OnExitState.

Entry state ed exit state

Non sempre si sente la necessità di eseguire del codice solo all’ingresso di uno stato (o case), ma quando si scopre che, implementarli è semplice, gli si trova utilità. Alcune applicazione necessitano di entry state e quindi non se ne può fare a meno di implementarle. Mentre per lo exit state è più difficile trovare una utilità, comunque scopriamo cosa sono e come è semplice implementarli, usando l’applicazione già sviluppata in precedenza, poi decidiamo se, come, e quando usarli. C'è anche da considerare che l'implementazione condiziona il modo in cui possiamo usufruire di queste caratteristiche.

Uno stato è diviso in tre porzioni di codice:

  • La prima (entry state) porzione viene eseguita solo all’ingresso nello stato.
  • La seconda (potremmo chiamarla loop state o run state), viene eseguita ciclicamente e solitamente è presente una condizione di uscita dallo stato.
  • La terza (exit state) si comporta in modo simile a entry state, ma il codice viene eseguito dopo che run state ha selezionato il prossimo stato. In altre parole, si esegue del codice, quando si esce dallo stato, indipendentemente da quale sia lo stato successivo, anche questa porzione viene eseguito una sola volta.

Ora è più chiaro perché On Entry State e On Exit State sono equivalenti di Entry state ed Exit state. Vediamo come appare lo pseudo codice dello stato START con entry ed exit state.

-> START
{
    ON_ENTRY_STATE {
        # codice eseguito solo all'ingresso dello stato
    }
    
    # Init run state
    # Accetta comando Play code:168
    # POLLING(attende pressione Play) 
    PLAY IS PRESSED?: GOTO S_INOPENING;
    # End run state
    
    ON_EXIT_STATE {
        # codice eseguito all'uscita dello stato.
    }
} 

Lo pseudo codice pecca di precisione in GOTO INOPENING, che lascia intendere un salto allo stato INOPENING, senza passare per ON_EXIT_STATE{}, cosa non corretta poiché deve prima eseguire ON_EXIT_STATE e abbandonare lo stato corrente per lo stato INOPENING. Al posto di GOTO avrei potuto usare FUTURE_STATE(INOPENING) che adesso sembra più adatto, ma anche SET_NEXT_STATE() sarebbe ancora meglio, ma l’influenza dai linguaggi di programmazione ha avuto la meglio per adesso.

La codifica richiede però delle variabili di supporto:

  • g_oldState è una variabile fotocopia di g_state
  • g_onEntryState è una variabile globale di tipo bool che diventa true per un solo ciclo di loop.

Vediamo prima il codice del case S_START con entry ed exit state:

        case S_START:
            /* ENTRY STATE */
            if (g_onEntryState) {
                Serial.println(F("Enter to S_START"));
            }
            /* RUN STATE */
            if (command == IRCMD_PLAY) {
                g_state = S_INOPENING;     
            } 
            /* EXIT STATE */
            if (g_state != g_oldState) {
                Serial.println(F("Exit from S_START"));
            }         
            break;

Due sole if (…) sono necessarie per ogni stato per ottenere ciò che vogliamo. Mentre nel loop dobbiamo aggiungere del codice per fare si che g_onEntryState sia vera per un solo ciclo di loop. Vediamo il codice da inserire all’inizio della funzione loop:

    
    if (g_oldState != g_state) {
        g_oldState = g_state;
        g_onEntryState = true;
    } else {
        g_onEntryState = false;
    }

Semplice semplice, quasi non serve commentare, ma qualcosa la scrivo in merito. Il meccanismo delle due variabili (g_state e g_oldState) non in sincro è ampiamente usato da sempre e lo uso nel modo classico; se le due variabili sono differenti, le rendo uguali e mi appunto ciò nella variabile g_onEntryState che diventa true. Al prossimo ciclo g_oldState e g_state sono uguali per cui g_onEntryState ritorna a false.

Devo solo fare attenzione a non modificare la variabile g_oldState all’interno di uno stato. All’interno dello stato uso lo stesso meccanismo delle variabili non in sincro per implementare lo exit state. Ma devo anche garantire che le due variabili siano diverse all’avvio dell’applicazione per cui la variabile globale g_oldState e dichiarata e inizializzata così:

    uint8_t   g_oldState    = 255;

In verità devo fare attenzione a non modificare anche altre variabili che è lecito potere leggere ma non scrivere. I problemi seri si presentano quando il numero di variabili che non devono essere scritte è tale da portarmi in errore. Dobbiamo considerare che oltre alle variabili che gestiscono il funzionamento degli stati ci sono anche le variabili usate dall’applicazione. Più il numero di variabili globali cresce più cresce la probabilità di commettere errori. Potremmo essere tentati dal dare un nome particolare alle variabili che non devono essere scritte, ma il linguaggio ci fornisce strumenti più flessibili e potenti, perché non usarli? Ma di questo avrò forse modo in futuro di scrivere qualcosa in merito, per adesso ci dobbiamo accontentare dello switch case e delle variabili globali.

Adesso è arrivato il momento di osservare il codice sorgente completo di entry ed exit state per farci una idea complessiva della complessità attuale.

    /*
       parking-access-s1 is demo application of parking access.
          
            Copyright (C) 2022 Maurilio Pizzurro 

       This program is free software; you can redistribute it and/or modify
       it under the terms of the GNU General Public License as published by
       the Free Software Foundation; either version 1, or (at your option)
       any later version.

       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 library.  If not, see <http://www.gnu.org/licenses/>.
    */

/* 
    Pulsante verde per simulare la barriera IR
    Pulsante play sul telecomando per alzare la barra.
*/

#include <IRremote.h>
#include <LiquidCrystal.h>
#include <Servo.h>

#define PIN_RECEIVER 2   // Signal Pin of IR receiver

IRrecv receiver(PIN_RECEIVER);
Servo RCservo;

LiquidCrystal lcd(12, 11, 10, 9, 8, 7);

/* SERVO_UP servo in posizione verticale */
#define SERVO_UP      180
/* SERVO_DOWN servo in posizione orizzontale */
#define SERVO_DOWN    90

/* S_XXX stati della macchina a stati g_state */
#define S_START       0
#define S_INOPENING   1
#define S_ISUP        2
#define S_INCLOSING   3

#define IRCMD_PLAY  168

// g_state è la variabile di stato
uint8_t   g_state       = S_START;
uint8_t   g_oldState    = 255;
uint8_t   g_servoPos    = SERVO_DOWN;
uint32_t  g_saveMillis  = 0;
bool      g_onEntryState    = false;

/* PIN_IR_BARRIER contatto pulito relay IR RX */
const byte PIN_IR_BARRIER   = 4;

void setup()
{
    Serial.begin(115200);
    pinMode(PIN_IR_BARRIER, INPUT_PULLUP);
    
    receiver.enableIRIn(); // Start the receiver
    lcd.begin(16, 2);

    Serial.print("Press play or\nyellow button\n");
    RCservo.attach(5);
    RCservo.write(g_servoPos);
    
}

void loop() {

    uint8_t command = 0;

    if (receiver.decode()) {
        command = receiver.decodedIRData.command;
        receiver.resume();  // Receive the next value
    }
    /* Gestione automatica degli entry state */
    if (g_oldState != g_state) {
        g_oldState = g_state;
        g_onEntryState = true;
    } else {
        g_onEntryState = false;
    }
        
    switch (g_state) {
        
        case S_START:
            /* ENTRY STATE */
            if (g_onEntryState) {
                Serial.println(F("Enter to S_START"));
            }
            
            /* RUN STATE */
            if (command == IRCMD_PLAY) {
                g_state = S_INOPENING;     
            } 
            
            /* EXIT STATE */
            if (g_state != g_oldState) {
                Serial.println(F("Exit from S_START"));
            }         
            break;

        case S_INOPENING:
            /* ENTRY STATE */
            if (g_onEntryState) {
                Serial.println(F("Enter to S_INOPENING"));
            }
            
            /* RUN STATE */          
            if (millis() - g_saveMillis > 20) {
                g_saveMillis = millis();
                
                if (g_servoPos <= SERVO_UP) {
                    g_servoPos++;
                    RCservo.write(g_servoPos);
                } 
                if (g_servoPos == SERVO_UP) {
                    /* CHANGE STATE */
                    g_state = S_ISUP;
                    g_saveMillis = millis();
                }
            }
            
            /* EXIT STATE */
            if (g_state != g_oldState) {
                Serial.println(F("Exit from S_INOPENING"));
            } 
            break;

        case S_ISUP:
            /* ENTRY STATE */
            if (g_onEntryState) {
                Serial.println(F("Enter to S_ISUP"));
            }
            
            /* RUN STATE */
            if (millis() - g_saveMillis > 2000) {
                /* CHANGE STATE */
                g_state = S_INCLOSING;
            }
            /* CHECK IR BARRIER */
            if (digitalRead(PIN_IR_BARRIER) == LOW) {
                /* RESTART TIMER */
                g_saveMillis = millis();
            }
            
            /* EXIT STATE */
            if (g_state != g_oldState) {
                Serial.println(F("Exit from S_ISUP"));
            }
            break;

        case S_INCLOSING:
            /* ENTRY STATE */
            if (g_onEntryState) {
                Serial.println(F("Enter to S_INCLOSING"));
            }
            
            /* RUN STATE */
            if (millis() - g_saveMillis > 35) {
                g_saveMillis = millis();
                  
                if (g_servoPos >= SERVO_DOWN) {
                    g_servoPos--;
                    RCservo.write(g_servoPos);
                } 
                if (g_servoPos == SERVO_DOWN) {
                    /* CHANGE STATE */
                    g_state = S_START;
                }
            }
            /* CHECK IR BARRIER */
            if (digitalRead(PIN_IR_BARRIER) == LOW) {
                /* CHANGE STATE */
                g_state = S_INOPENING;
            }
            
            /* EXIT STATE */
            if (g_state != g_oldState) {
                Serial.println(F("Exit from S_INCLOSING")); 
            }
            break;
    } // end switch
} // end loop()

Per fare compiere qualcosa agli Entry e Exit state ci ho messo la stampa su seriale, così stamperà lo stato in cui entra e quello che da cui esce. In questo modo non intaso la seriale di stampe tutte identiche ma soltanto due stampe per stato, una all’ingresso e l’altra in uscita.

Tiriamo le somme, così si dice, abbiamo 5 variabili globali, 2 delle quali gestiscono gli entry state, cioè g_oldState e g_onEntryState e sono critiche.

Il programma conta 168 righe inclusi gli spazi. Il codice all’interno di un case non supera la dimensione di una schermata.

Abbiamo ancora alcune macro che potremmo pure trasformare in enumerati tramite la parola chiave enum.

Sebbene la situazione sembri sotto controllo, ci vuole poco perché diventi quasi ingestibile. Il problema è che facciamo tante cose in una porzione di codice delimitata dalla funzione loop. Anche se ci sono solo 2 variabili critiche, sono li a portata di mano del distratto di turno, ma noi vogliamo assaggiare cosa significa darsi una martellata sul dito per poi imparare a piantare chiodi, quindi farciremo il loop di codice, nell’attesa che arrivi il momento, in cui faremo fatica a seguire il codice e prima o poi, questo momento arriverà.

Farciamo il loop

Cosa ancora gli possiamo fare compiere? Vediamo proviamo a fare una lista di funzionalità mancanti più o meno utili:

  1. Visualizza sul display la posizione del servo RC anche e specie quando si muove.
  2. Lampeggiante a due led, uno rosso e uno giallo.
  3. Visualizzare sul display il conto alla rovescia del timeout di 2 secondi, cosi da visualizzare: 1999, 1998. 1997 ecc, fino a zero. Così possiamo vedere ancora meglio cosa accade se premiamo il pulsante verde.
  4. Conteggio delle aperture e chiusure della barra. Ma a cosa ci serve? a nulla, anche se nella realtà serve per fare la manutenzione programmata, di cui non abbiamo necessità, lo implementiamo lo stesso per farcire il loop.
  5. Aggiunta di un pulsante giallo che simula il contatto chiave; se il telecomando ha le pile scariche posso entrare con la chiave e mi pare il minimo.
  6. Durante l’esecuzione dello stato S_START voglio che dopo una inattività di 50 secondi si passi allo stato IDLE, mettendo la MCU a nanna (in sleep).
  7. C’è un display, usiamolo come si vede fare spesso, per i 50 secondi prima di andare a nanna, gli facciamo fare una animazione scorrevole.

Mi fermo qui solo perché già mi sembra abbastanza e c’è il rischio che se ci rifletto altri 10 minuti tiro fuori altri 5 o 6 punti.

Visualizza la posizione del servo

Il punto (1) si ottiene con poca fatica, senza aggiungere alcuna variabile globale. Ci serve solo una funzione di supporto di nome: void lcdPrintPos(uint8_t pos). Potremmo fare a meno dell’argomento pos, poiché la variabile g_servoPos è globale, ma non è mai cosa buona creare funzioni che usano variabili globali, ma il motivo principale e che osservando il case in cui avviene la chiamata, si nota subito che gli viene passato come argomento il valore di g_servoPos. Se per ipotesi g_servoPos non esistesse e per ottenere la posizione del servo dovessi chiamare il metodo getPos() di un oggetto ctrlBar, potrei continuare ad usare la funzione chiamandola così: lcdPrintPos(ctrlBar.getPos()). Quindi ci sono ottimi motivi per lasciare li al suo posto l’argomento pos. Bene vediamo il codice di questa funzione di supporto:

    void lcdPrintPos(uint8_t pos) {
        char posBuff[4];
        posBuff[ 4 - 1 ] = '\0';
        sprintf(posBuff, "%#3u", pos);
        lcd.setCursor(1, 1);
        lcd.print(posBuff);
    }

Semplice, breve e usa solo variabili locali, cosa si può volere di più? La funzione si limita a visualizzare su lcd il valore di una variabile grande 1 byte che al massimo può assumere valore 255 decimale, ed è composta da 3 caratteri al massimo. Vista la genericità e la indipendenza di questa funzione, potremmo pure dargli un nome più generico e aggiungere gli argomenti row e col così da usarli in più occasioni. Lasciamo le cose come stanno al momento, in futuro se avessimo necessità, apporteremo le modifiche per renderla generica.

Non resta che inserire la chiamata della funzione nei due case che modificano la posizione del servo, e questi sono: S_INOPENING e S_INCLOSING. Qui a seguire solo la porzione RUN STATE di S_INOPENING:

            /* RUN STATE */         
            if (millis() - g_saveMillis > 20) {
                g_saveMillis = millis();
                
                if (g_servoPos <= SERVO_UP) {
                    g_servoPos++;
                    RCservo.write(g_servoPos);
                    lcdPrintPos(g_servoPos);    // La chiamata in questione.
                } 
                if (g_servoPos == SERVO_UP) {
                    /* CHANGE STATE */
                    g_state = S_ISUP;
                    g_saveMillis = millis();
                }
            }

Qui a seguire sempre solo la porzione RUN STATE di S_INCLOSING.

            /* RUN STATE */
            if (millis() - g_saveMillis > 35) {
                g_saveMillis = millis();
                  
                if (g_servoPos >= SERVO_DOWN) {
                    g_servoPos--;
                    RCservo.write(g_servoPos);
                    lcdPrintPos(g_servoPos);
                } 
                if (g_servoPos == SERVO_DOWN) {
                    /* CHANGE STATE */
                    g_state = S_START;
                }
            }
            /* CHECK IR BARRIER */
            if (digitalRead(PIN_IR_BARRIER) == LOW) {
                /* CHANGE STATE */
                g_state = S_INOPENING;
            }

Ci manca soltanto chiamare la stessa funzione nel setup(). Il risultato finale sul display sarà qualcosa di simile a quello in Figura 1.

Bene, questa è fatta e non me la sono sentita di farcire i 2 case con il codice della funzione per poi mostrare che avremmo fatto meglio a creare una funzione di supporto.

Figura 1: Screenshot del progetto su wokwi

Lampeggiante a due led

Il segnalatore luminoso di movimento o semplicemente lampeggiante è composto da 2 led, uno rosso e uno giallo. Servono 2 pin liberi a scelta, io ho scelto il pin 13 (a cui è già connesso il led contrassegnato con la sigla L sulla board. L’altro pin vicino è A0, ma può benissimo essere impiegato in digitale. Servono due resistori da 330 ohm (470÷1000 ohm) ognuno in serie tra led e pin. Lo schema di collegamento è mostrato in Figura 1, dove compare anche il pulsante giallo che simula il contatto sotto chiave.

Aggiungiamo alla funzione setup() le pinMode() dei due pin come output:

    pinMode(LED_BUILTIN, OUTPUT);
    pinMode(A0, OUTPUT);

Ora ci serve una funzione di supporto la cui chiamata verrà inserita nella funzione loop(), vediamola subito questa funzione:

    void flashingLight(bool tf) {
        #define FL_TIMING 200
        static uint32_t timer = 0;
        static uint8_t timing = 0;
        if (!tf) {
            digitalWrite(LED_BUILTIN, LOW);
            digitalWrite(A0, LOW);
            timer = 0;
            timing = 0;
            return;
        }
          
        if (millis() - timer > timing) {
            bool yelIsOn = !digitalRead(A0);
            timer = millis();
            timing = FL_TIMING;
            digitalWrite(LED_BUILTIN, !yelIsOn);
            digitalWrite(A0, yelIsOn);
        }

    }

Da commentare ci sono le due variabili statiche timer e timing che non è necessario siano globali. Se la chiamata equivale a: flashingLight(0) la if (!tf) { verrà eseguita e questa spegne entrambe i led, azzera le due variabili e restituisce il controllo al chiamante. Se al posto dell’argomento 0 c’è un valore differente da 0 (1, 2, 3 ecc) viene eseguita la seconda if temporizzata con millis(), dove però timer e timing hanno valore 0, per cui si entra subito e vediamo che alle due variabli in questione gli vengono assegnati dei valori. Inoltre qui dobbiamo accendere in modo alternato i led, cioè ad ogni chiamata della funzione, il led che era acceso si spegne e quello che era spento si accende.

Viene da chiedersi; ma che argomento gli dobbiamo passare alla funzione? Guarda caso, combinazione vuole, che la variabile g_state assuma il valore zero, solo quando si sta eseguendo il case S_START, proprio quando serve il lampeggiante acceso, la variabile g_state ha valori diversi da zero, sembra fatto di proposito. Quindi la chiamata a funzione viene inserita all’inizio della funzione loop(), mostrata qui di seguito parzialmente:

    void loop() {
        flashingLight(g_state);
    
        uint8_t command = 0;

        if (receiver.decode()) {

Non sembra vero, c’è la siamo cavati con poco codice, una sola riga inserita nella funzione loop(). Non dimentichiamolo, si ottiene ciò grazie alle funzione di supporto flashingLight(). Nonostante mi stia impegnando nell’intento di farcire il loop di codice, qualcosa me lo impedisce, ma non demordo.

Visualizzare il timeout su LCD

Anche questo punto può essere risolto implementando una funzione di supporto. Vediamola questa funzione:

    void lcdPrintTimeout(uint16_t t) {
        char timeBuff[5];
        timeBuff[ 5 - 1 ] = '\0';
        sprintf(timeBuff, "%#4u", t);
        lcd.setCursor(11, 1);
        lcd.print(timeBuff);
    }

La chiamata avviene solo nel case S_ISUP quindi potremmo pure copiare il codice della funzione dentro al case. Tuttavia dentro ai case non è permesso dichiarare variabili locali, se lo si desidera si può aggirare questa limitazione aggiungendo un blocco di codice { }, il case apparirebbe così:

    case S_ISUP:
    {
        char timeBuff[5];
        timeBuff[ 5 - 1 ] = '\0';
        // ecc
    
    }

In questo caso lo sconsiglio e continuo a preferire la funzione di supporto. Bene vediamo la chiamata, di seguito viene mostrato solo il run state del case S_ISUP:

            /* RUN STATE */
            if (millis() - g_saveMillis > 2000) {
                /* CHANGE STATE */
                g_state = S_INCLOSING;
                lcdPrintTimeout(0);
            } else {
                uint32_t dt = millis() - g_saveMillis;
                lcdPrintTimeout(2000 - dt);    // ecco la chiamata a funzione
            }
            /* CHECK IR BARRIER */
            if (digitalRead(PIN_IR_BARRIER) == LOW) {
                /* RESTART TIMER */
                g_saveMillis = millis();
            }

Grazie al blocco di codice else {} posso creare la variabile locale dt che mi serve per chiamare la funzione lcdPrintTimeout(). Poco prima di } else { c’è la chiamata a lcdPrintTimeout(0) per aggirare un comportamento non desiderato evidenziato durante i test. Il problema è che alle volte il display mostra un timeout residuo di 1 millesimi di secondo ed esce dal case S_ISUP, mentre io voglio che mostri 0 millesimi e poi può uscire dal case. Questo problema dovevo aspettarmelo, ma me lo sono tolto dalla testa, per fortuna, ho la funzione di supporto su cui posso contare.

Mentre il conto alla rovescia (partendo da 2000) viene visualizzato sul display, premo il pulsante verde, e sul display si vede ripartire, da 2000 il conto alla rovescio. Era ciò che volevo ottenere e quindi anche questo punto è implementato.

Il punto 4

Questo punto sarebbe semplice da implementare su una scheda vera, ma con wokwi la eeprom non mantiene le informazioni tra una simulazione e l’altra, per cui questo punto già poco utile non sarà implementato.

Apertura con chiave

Comandare il sollevamento della barra da un contatto chiave è facile, basta aggiungere un pulsante e collegarlo ad un pin. In Figura 1 si vede il pulsante giallo collegato al pin D6. Il codice è davvero semplice, non dimentichiamo la pinMode() nel setup(). Il codice parziale del loop() è sufficiente per mostrare come lavora:

    void loop() {
        flashingLight(g_state);

        uint8_t command = 0;

        if (receiver.decode()) {
            command = receiver.decodedIRData.command;
            receiver.resume();  // Receive the next value
        } else if (digitalRead(PIN_KEY) == LOW) {
            command = IRCMD_PLAY;
        } 

In breve, la variabile command può assumere un valore proveniente da due sorgenti. La prima sorgente è il ricevitore IR. La seconda sorgente è il pulsante giallo.

Il punto 6

Non si può avere tutto, sembra che il simulatore wokwi non implementi il risparmio energetico della MCU, per cui non posso mettere la MCU a nanna (sleep).
Ma non è un grosso problema, l'applicazione non è reale, se lo fosse mi dovrei preoccupare di togliere l'alimentazione al servo motore in modo da ridurre il consumo di energia in modo considerevole. Se questa applicazione non reale, dovesse trovare applicazione pratica in un plastico e volessimo risparmiare energia, potremmo quanto meno usare il metodo detach(), ma prima di muovere il servo dovremmo inserire attach(pin).

Conclusione

Concludo qui saltando il settimo punto perché ancora non implementato. Devo dire che, non avere potuto implementare i punti 4 e 6 mi ha destabilizzato e ora non so come sfruttare meglio la presenza del display. Si vedrà in seguito quando fare comparire la scritta scorrevole e il suo contenuto. Di sicuro lo stato S_START è quello in cui si permane per lungo tempo a patto di non premere nulla, per cui S_START è il cadidato ideale in cui fare comparire delle scritte scorrevoli. Congelare il codice a questo stadio dello sviluppo è pure utile, poiché si hanno le funzionalità principali con una implementazione ancora chiara e da questa implementazione possiamo derivarne altre, ad esempio mettendo il tutto dentro una o più classi C++. Per coloro i quali fossero interessati alle scritte scorrevoli lascio il link ad un articolo che affronta il problema. L'applicazione qui descritta come sempre la potete sperimentare su wokwi seguendo questo link.

Licenza Creative Commons
Quest'opera è distribuita con Licenza Creative Commons Attribuzione 4.0 Internazionale

Nessun commento:

Posta un commento