Una "semplice” automazione, bugia, non è per niente semplice scrivere una applicazione come questa.
Le difficoltà da superare sono molteplici:
- Il programma deve comportarsi come desiderato.
- Il programma deve essere manutenibile ed estendibile.
- Il codice deve essere chiaro e comprensibile.
Questi punti non sono requisiti, ma punti cardine che ogni applicazione deve rispettare. Se il programma rispetta il punto (1), ma fallisce sul punto (2), si dovrà trovare il modo di apportare modifiche, per centrare il punto (2), senza compromettere il punto (1). In breve, qualunque sia il motivo che ti spinge a modificare il codice, devi essere consapevole, che nell’intento di rispettare il punto (2), puoi facilmente compromettere il punto (1). Stessa cosa invertendo (1) e (2).
I requisiti
Questa applicazione non ha alcun requisito, poiché non è reale e concreta. Non si ha l’obbligo (e responsabilità) di rispettare alcuna normativa di legge, nessuna certificazione è necessaria. Tutto ciò è una enorme semplificazione, nonostante ciò, sarà necessario investire del tempo, tempo che non ci manca certo, poiché faremo degli errori, ripareremo a questi e faremo tesoro di ciò che abbiamo imparato. Alla fine ci illuderemo di avere rispettato tutti i punti cardine.
L’hardware necessario
Alcuni componenti possono essere sostituiti, ad esempio il ricevitore IR e il telecomando possono essere sostituiti con un pulsante. La coppia fotocellule IR RX e TX, qui è sostituita con un pulsante verde. La fotocellula RX contiene un relay i cui contatti si chiudono se il fascio IR viene interrotto, in altre parole, se il sensore IR non riceve il fascio IR trasmesso da TX, i contatti del relay sono chiusi. Questo può accadere anche per un mancato allineamento ottico tra TX ed RX. I contatti del relay sono sostituiti dai contatti normalmente aperti del pulsante verde.
Screenshot del progetto sviluppato con il simulatore wokwi. |
Elenco componenti
1 Arduino qualunque, la UNO, nano, MEGA.
1 Display LCD 2 o 4 righe.
1 RC Servo motore per simulare la barra.
1 Pulsante per simulare la coppia di fotocellule IR.
1 Pulsante PLAY o ricevitore IR più telecomando.
Descrizione dell’applicazione
Qualunque descrizione che impieghi Italiano, Inglese ecc, anche se è dovuta, non è sufficiente per esprimere tutti dettagli senza rischi di essere fraintesi. Per tale motivo, è bene fornire una descrizione impiegando un linguaggio alternativo, che si presti a descrivere in modo sintetico, anche i dettagli. Uno di questi linguaggi è detto pseudo linguaggio, purtroppo non esiste uno standard da seguire per esprimersi universalmente.
Descrizione in linguaggio corrente
Il telecomando verrà usato per azionare l’automazione, poiché è sufficiente un tasto (ad es. Play), il telecomando e il ricevitore IR, possono essere sostituiti con un semplice pulsante.
Il servo RC simula la barriera che consente o nega l’accesso al parcheggio. In questo caso, l’automazione consente l’accesso a chi è dotato di telecomando.
La barriera IR o coppia di fotocellula (TX, RX), qui viene sostituita dal pulsante verde. Il pulsante premuto equivale, ad interruzione della barriera e ciò indica il transito.
Premuto il tasto “Play” la barra si posiziona in verticale permettendo il transito di veicoli.
La barra resta in verticale 2 secondi, dopo torna lentamente in orizzontale. Durante i 2 secondi di attesa si può verificare l’interruzione della barriera IR (pulsante verde premuto), in questo caso il tempo di attesa si somma al tempo già trascorso; ad esempio 1.2s già trascorsi più 2 secondi dalla pressione del pulsante verde.
Tenere premuto il pulsante verde equivale a sostare tra le fotocellule, in questa condizione la barra rimarrà in verticale. La barra inizierà a scendere in orizzontale 2 secondi dopo avere rilasciato il pulsante verde.
La pressione sul pulsante durante la discesa della barra comporta inversione di rotazione e la barra si porta in verticale.
Pseudo linguaggio
La descrizione precedente pare già sufficientemente dettagliata, tuttavia non fornisce un contributo sufficiente a formulare un algoritmo, cosa che, prima o poi dovremo fare, per cui, usiamo un linguaggio che è una via di mezzo, tra la lingua di cui abbiamo maggiore padronanza e il linguaggio di programmazione di cui abbiamo maggiore padronanza.
Da quanto detto, nasce lo pseudo linguaggio, il quale dovrebbe essere totalmente estraneo nei confronti dei linguaggi di programmazione informatica. Tuttavia è difficile non esserene influenzati, per cui, istanze come: BEGIN, IF, GOTO, END, FOR ecc sono frequentemente usate.
Nel caso specifico ho preferito istanze come:
GREEN BUTTON
{
ISPRESSED?
NOPRESSED?
}
Nota il punto di domanda ?, che possiamo leggere facilmente GREEN BUTTON IS PRESSED?
La stessa istanza GREEN BUTTON può essere espansa in:
GREEN BUTTON
{
ISPRESSED?
{
# qui il codice da eseguire in caso il pulsante verde risulta premuto
}
NOPRESSED?
{
# codice nel caso il pulsante verde non è stato premuto
}
}
Per inserire commenti ho usato il carattere cancelletto derivato dal linguaggio python. Qui a seguire, lo pseudo codice che descrive l’algoritmo della automazione in questione.
Pseudo codifica
# Pseudo codice automazione controllo barra accesso a parcheggio.
-> START
{
# Accetta comando Play code:168
# POLLING(attende pressione Play)
PLAY IS PRESSED?: GOTO INOPENING;
}
-> INOPENING
{
ACT(RCSERVO, UP)
IS SERVO UP?
{
YES: SAVETIME, GOTO ISUP;
NOT: ---
}
}
-> ISUP
{
GREEN_BUTTON
{
ISPRESSED?: SAVETIME;
NOTPRESSED: ---
}
IS_TIMEOUT?(2s)
{
GOTO INCLOSING;
}
}
-> INCLOSING
{
ACT(RCSERVO, DOWN)
IS SERVO DOWN?
{
GOTO START;
}
GREEN_BUTTON
{
ISPRESSED?: GOTO INOPENING;
NOTPRESSED: ---
}
}
Pseudo codifica di START
Dato che non esiste uno standard sullo pseudo linguaggio possiamo scrivere quello che vogliamo, ma attenzione ad essere coerenti e rispettare la sintassi.
La descrizione dell’algoritmo in questione appena vista è molto distante dalla codifica reale ed è anche superficiale. Se lo si desidera, si possono aggiungere commenti per indicare il grado di superficialità, ad esempio diamo per scontato che la descrizione precedente sia buona, ma ne vogliamo creare un altra meno superficiale punto per punto, iniziando da START.
START # is a state of the finite state machine g_state
{
IF (IR.COMMAND == PLAY)
# CHANGE STATE
g_state = INOPENING
}
Da questo pseudo codice si intuisce che, START è uno degli stati della macchina a stati finiti (FSM) g_state. Anche INOPENING è uno stato delle stessa FSM. In questo stato, la cpu esegue ripetutamente il codice che verifica se é stato premuto il tasto PLAY sul telecomando, infatti quando il ricevitore IR, riceve il comando COMMAND e questo è uguale al codice PLAY, g_state assume lo stato INOPENING. Già questa descrizione è più vicina alla codifica finale e ci fornisce maggiori informazioni utili per la codifica nel linguaggio scelto.
La codifica in C
La codifica in C la possiamo osservare di seguito:
case S_START:
if (command == IRCMD_PLAY) {
/* CHANGE STATE */
g_state = S_INOPENING;
}
break;
S_START
, IRCMD_PLAY
e S_INOPENING
sono macro del preprocessore, mentre g_state
è una variabile globale di tipo uint8_t
, qui a seguire il codice:
#define S_START 0
#define S_INOPENING 1
#define IRCMD_PLAY 168
uint8_t g_state = S_START;
La variabile command
è dichiarata locale nella funzione loop, il suo valore iniziale è zero. Se il ricevitore IR riceve un comando, questo viene assegnato alla variabile command
.
Codifica C parziale
Arrivati a questo punto può tornare utile osservare il codice non completo (ma funzionante) dello sketch:
#include <IRremote.h>
#define PIN_RECEIVER 2 // Signal Pin of IR receiver
IRrecv receiver(PIN_RECEIVER);
#define S_START 0
#define S_INOPENING 1
#define IRCMD_PLAY 168
uint8_t g_state = S_START;
void setup() {
Serial.begin(115200);
receiver.enableIRIn(); // Start the receiver
}
void loop() {
uint8_t command = 0;
if (receiver.decode()) {
command = receiver.decodedIRData.command;
receiver.resume(); // Receive the next value
}
switch (g_state) {
case S_START:
if (command == IRCMD_PLAY) {
/* CHANGE STATE */
g_state = S_INOPENING;
}
break;
case S_INOPENING:
// Not implemented
Serial.print("INOPENING");
delay(1000);
/* CHANGE STATE */
g_state = S_START;
break;
}
}
Il case S_INOPENING
non è implementato, al suo posto abbiamo inserito la stampa su seriale di un testo, un delay e infine il cambio di stato che ci riporta al case S_START
. Se il programma si comporta come desiderato si può passare ad implementare il prossimo stato.
Lo stato INOPENING
Il case S_INOPENING
ha il compito di azionare il motore del servo RC fino a posizionarlo a 180 gradi.
Immaginiamo di usare, al posto del servo RC, un motore con encoder accoppiato sull’asse. Servirà controllare velocità di rotazione e posizione. Potere controllare la velocità di rotazione del motore in relazione alla posizione in cui si trova la barra, ci permette di attuare la movimentazione in modo dolce è progressivo, riducendo gli stress elettrici e meccanici. Al contempo dobbiamo anche garantire una certa rapidità di apertura e chiusura della barra. Avremo quindi una velocità di rotazione bassa iniziale, che incrementa in relazione alla posizione. Arriveremo così ad avere una velocità di rotazione massima di picco, che permane fino al punto in cui la velocità, si riduce progressivamente, fino ad arrestarsi nella posizione desiderata.
Per fortuna noi usiamo un servo RC, la cui velocità di posizionamento è fissa, diciamo per fortuna, poiché provate a fissare sul braccio del servo, un asta lunga 30 cm, l’arresto alla posizione desiderata avverrà in brevissimo tempo e l’asta oscillerà, stessa cosa nel verso opposto. Potremo fare finta di nulla e risolvere con un semplice RCservo.write(180)
, invece vogliamo incrementa la posizione di un grado ogni 35ms, ciò vuole dire, che per compiere una rotazione di 90°, ci vorranno: 90° x 35ms = 3150ms
(~3s). Mentre la discesa della barra sarà ancora più lenta: 90° x 50ms = 4500
(4.5s). In ogni caso sarà possibile impostare una diversa velocità.
La codifica in C
Lo stato S_INOPENING
necessità di due variabili globali: g_saveMillis
e g_servoPos
e due macro: SERVO_UP
e S_ISUP
.
Grazie alla prima if (millis()...
il codice tra le graffe, viene eseguito ogni 35ms. g_servoPos
vale 90
e SERVO_UP
vale 180
, quindi g_servoPos
è minore di SERVO_UP
, per cui incrementiamo di 1 g_servoPos
, che adesso vale 91
, aggiorniamo la posizione del servo con RCservo.write(g_servoPos)
e così via, fino a che g_servoPos
vale 180
e siamo pronti per il prossimo stato, ma prima salviamo il tempo in g_saveMillis
. Al prossimo ciclo di loop, verrà selezionato il case S_ISUP
, non ancora implementato.
case S_INOPENING:
if (millis() - g_saveMillis > 35) {
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();
}
}
break;
Quando ci occuperemo di implementare il case S_INCLOSING
vedremo che è simile a S_INOPENING
appena descritto.
Avrete notato che aggiungo il commento CHANGE STATE
, nella riga che precede l’assegnazione, di un nuovo stato alla variabile g_state
. Potrà sembrarvi superfluo, ma non lo è, poiché i punti di uscita dallo stato corrente, ci servono per seguire il flusso di esecuzione. Inoltre, se uno o più case
avessero più uscite diverse tra loro, il codice probabilmente sarebbe composto da molte più righe e sarebbe ancora meno semplice seguire il flusso di esecuzione e se ci lasciassimo prendere la mano introducendo 20, 30, 50 righe all’interno di un case, dovremmo sentire una vocina che ci dice; non farlo, poi non ci capisci più nulla, prova invece a snellire il case
creando delle funzioni di supporto. Ma questo argomento lo affronteremo più avanti, proprio facendo quello che non dovremmo.
Lo stato ISUP
Questo stato si occupa di monitorare gli eventi. Due sono gli eventi di cui soltanto uno determina lo stato di uscita:
- È scaduto il timeout di 2 secondi.
- Il pulsante verde è stato premuto.
I due secondi di timeout sono conteggiati a partire dall’istante in cui la barra è in posizione verticale (180). Durante questo intervallo di tempo, la pressione del pulsante verde, ricarica il timeout di 2 secondi. Sarà pertanto possibile permanere in questo stato un tempo infinito, mantenendo premuto il tasto verde. Questo comportamento non è evitabile, tuttavia, il permanere in questo stato per lungo tempo, potrebbe essere annotato in una tabella degli errori, consultabile attraverso degli strumenti software di diagnosi impiegati dal tecnico della manutenzione. Poiché questa non è una applicazione reale, non ci poniamo il problema e non gestiremo alcuna tabella degli errori.
La codifica in C
La codifica in C, come si può osservare è molto semplice ed esplicita, tanto che ogni ulteriore commento è superfluo. Risulta evidente che il prossimo stato da implementare è S_INCLOSING
.
case S_ISUP:
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();
}
break;
Lo stato INCLOSING
Come detto prima, lo stato INCLOSING
è simile allo stato INOPENING
, con la differenza che g_servoPos
deve essere decrementata di 1. In aggiunta dobbiamo prevedere che, durante il movimento della barra, un veicolo o persona interrompa il fascio IR, simulato dalla pressione del pulsante verde. Nel caso ciò si verifichi, cediamo il controllo allo stato INOPENING,
invertendo così il movimento della barra.
Mentre se il pulsante verde non viene premuto, la barra arriverà alla posizione verticale (90°) e possiamo cedere il controllo allo stato START
.
La codifica in C
La codifica in C è molto simile al case INOPENING
, come si vede, al posto di g_servoPos++
c’è g_servoPos--
. Ovviamente anche la condizione va modificata in modo che, g_servoPos
non possa assumere valore inferiore a SERVO_DOWN
.
Il controllo CHECK IR BARRIER
è identico a quello precedente, con la differenza che, adesso cediamo il controllo al case S_INOPENING
.
case S_INCLOSING:
if (millis() - g_saveMillis > 50) {
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;
}
break;
Conclusione
Questa implementazione non è l’unica possibile e non vuole essere la migliore implementazione in assoluto, invece vuole essere la più adatta per fini didattici, e lo è, poiché mostra in modo esplicito e sintetico tutti i meccanismi. Possiamo dire di avere rispettato tutti i punti cardine, o almeno ci siamo sforzati di rispettarli con discreto successo, complice il fatto che l’applicazione è semplice. Da questa implementazione possiamo derivarne altre, magari nascondendo i meccanismi dentro le classi C++, ma non è questa la strada che prenderò.
L’implementazione di una macchina a stati finiti tramite lo switch case si dimostra essere valida almeno fintantoché gli stati si possono contare sulle dita di una mano. Volendo è possibile semplificare lo switch case impiegando delle funzioni di supporto, invece lo complicherò al fine di verificare e constatare quando abbiamo raggiunto il limite. L’implementazione della macchina a stati attuale è priva di entry state ed exit state, vedremo come implementare gli entry state e quanto sono utili.
La attuale applicazione è mancante di alcune caratteristiche, ad esempio manca il segnalatore luminoso di movimento, detto lampeggiante, potremmo pure aggiungere la segnalazione sonora di movimento. Sicuramente aggiungerò un pulsante per simulare un contatto chiave con cui sollevare la barra.
Il display è stato usato marginalmente vedremo come usarlo meglio implementando gli entry state.
Codice sorgente completo
Per completezza allego a seguire il codice completo che potete provare online su wokwi.
Applicazione accesso a parcheggio - Entry ed Exit state
Quest'opera è distribuita con Licenza Creative Commons Attribuzione 4.0 Internazionale
Nessun commento:
Posta un commento