Ora esamineremo la logica funzionale dei dispositivi.
Il Server ha il comando della situazione e deve controllare la pompa con un relè e aspettarsi lo stato dal client. Per il sistema di sicurezza inseriamo un limite orario della pompa attiva se si verificano dei problemi, oppure è possibile utilizzare una modalità ping che prevede un messaggio di keep-alive ogni 10 secondi se non ci sono messaggi in quel periodo.
Configurazione EByte E32
Il dispositivo LoRa è un E32 ed è configurato per una trasmissione fissa con un tempo di sveglia elevato così da inviare un lungo preambolo per garantire che il client sia riattivato.
Fare riferimento all’articolo “Ebyte LoRa E32 per Arduino, esp32 o esp8266: configurazione“.
ResponseStructContainer c;
c = e32ttl.getConfiguration();
Configuration configuration = *(Configuration*) c.data;
configuration.ADDL = SERVER_ADDL;
configuration.ADDH = SERVER_ADDH;
configuration.CHAN = SERVER_CHANNEL;
configuration.OPTION.fixedTransmission = FT_FIXED_TRANSMISSION;
configuration.OPTION.wirelessWakeupTime = WAKE_UP_2000;
configuration.OPTION.fec = FEC_1_ON;
configuration.OPTION.ioDriveMode = IO_D_MODE_PUSH_PULLS_PULL_UPS;
configuration.OPTION.transmissionPower = POWER_20;
configuration.SPED.airDataRate = AIR_DATA_RATE_010_24;
configuration.SPED.uartBaudRate = UART_BPS_9600;
configuration.SPED.uartParity = MODE_00_8N1;
ResponseStatus rs = e32ttl.setConfiguration(configuration, WRITE_CFG_PWR_DWN_SAVE);
SERIAL_DEBUG.println(rs.getResponseDescription());
Pulsanti e relè
Menu e azioni sono gestiti da un pcf8574 e da un encoder, e spiego meglio il cablaggio e il software nel mio tutorial “PCF8574 un expander i2c I/O digitale: Arduino, esp8266 e esp32, encoder rotativo”.
Ma devi prestare attenzione alla gestione delle interruzioni sul canale i2c condiviso. In questo caso devo disabilitare e riattivare l’interrupt pcf8574 durante l’aggiornamento del display i2c, puoi vedere sul relativo articolo dell’ssd1306.
pcf8574.detachInterrupt();
if (changedButton) {
currentScreen = clickMenu(currentScreen, encoderValue);
changedButton = false;
}
renderScreen(currentScreen);
display.clearDisplay();
pcf8574.attachInterrupt();
Il relè è gestito da un unico pin e sincronizzato in tempo reale.
void startPump(){
DEBUG_PRINTER.println("START PUMP");
pumpIsActive = true;
digitalWrite(RELAY_PIN, HIGH);
pumpStartTime = millis();
lastMillisMessageReceived = millis();
}
void stopPump(){
DEBUG_PRINTER.println("STOP PUMP");
pumpIsActive = false;
digitalWrite(RELAY_PIN, LOW);
pumpStopTime = millis();
}
Menù su display
Uso un display Oled ssd1306, molto piccolo, qui la guida completa alle animazioni e alla generazione delle icone “Display OLED SDD1306: immagini, splash e animazioni”.
Gestirò un set di Icone per capire meglio l’operazione. Cambio l’icona del menu ruotando un encoder e selezionandolo facendo clic sull’encoder.
Gestisco il multilingua includendo un file con tutte le etichette
#include "include/ita.h"
//#include "include/eng.h"
Uso una semplice macchina a stati per gestire il Display; un interruttore gestito dal valore dell’encoder viene utilizzato per visualizzare l’icona corretta; i valori sono:
PUMP_START_MENU
: avviare la pompa e attendere che sia piena; l’unica sicurezza aggiuntiva è un timer che arresta la pompa dopo un’ora.
PUMP_START_PING_MENU
: avvia la pompa e attendere un messaggio ping ogni 20 secondi; se il serbatoio è pieno o non viene visualizzato alcun messaggio dopo 20 secondi, arresta la pompa.
PUMP_STOP_MENU
: ferma la pompa e dice al cliente di entrare in modalità di sospensione.
STATUS_MENU
: stato grafico dei dispositivi (pompa attiva, stato batteria e livello serbatoio).
STATUS_MENU_DETAILED
: stato descrittivo dei dispositivi.
void mainMenu(String title = SCREEN_MAIN_TITLE) {
display.clearDisplay(); //for Clearing the display
switch (encoderValue) {
case PUMP_START_MENU:
displayIconLabel(START_PUMP_LABEL, 2);
display.drawBitmap(70, 5, tapDrop, 54, 54, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color)
break;
case PUMP_START_PING_MENU:
displayIconLabel(START_PUMP_PING_LABEL, 3);
display.drawBitmap(70, 5, containerDrop, 54, 54, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color)
break;
case PUMP_STOP_MENU:
displayIconLabel(STOP_PUMP_MENU_LABEL, 2);
display.drawBitmap(70, 5, dropSlash, 54, 54, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color)
break;
case STATUS_MENU:
displayIconLabel(STATUS_DEVICE_MENU_LABEL, 1);
display.drawBitmap(70, 5, info, 54, 54, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color)
break;
case STATUS_MENU_DETAILED:
displayIconLabel(STATUS_DETT_MENU_LABEL, 1);
display.drawBitmap(70, 5, info, 54, 54, WHITE); // display.drawBitmap(x position, y position, bitmap data, bitmap width, bitmap height, color)
break;
default:
break;
}
display.display();
}
L’azione del clic è gestita da una funzione che utilizza la schermata corrente e il valore dell’encoder nella schermata corretta.
SCREEN clickMenu(SCREEN screen, int state){
SCREEN returningScreen = SCREEN_ERROR;
switch (screen) {
case SCREEN_MAIN:
returningScreen = mainMenuState((MENU_MAIN_STATE)state);
break;
case SCREEN_STATUS_DEVICE:
case SCREEN_STATUS_DEVICE_DETAILED:
case SCREEN_ERROR:
returningScreen = checkMenuState();
break;
default:
break;
}
encoderValue = 0;
screenEncoderSizeMax = screenEncoderSize[returningScreen];
SERIAL_DEBUG.print(F("CLICK --> "));
SERIAL_DEBUG.print( F("FROM SCREEN --> ") );
SERIAL_DEBUG.print( screen );
SERIAL_DEBUG.print(F(" - TO SCREEN --> "));
SERIAL_DEBUG.print( returningScreen );
SERIAL_DEBUG.print(F(" - MENU STATE --> "));
SERIAL_DEBUG.println( state );
return returningScreen;
}
Quindi, una funzione esegue il rendering della schermata corretta in base alla schermata selezionata:
void renderScreen(SCREEN screen){
switch (screen) {
case SCREEN_MAIN:
mainMenu();
break;
case SCREEN_STATUS_DEVICE:
checkPage();
break;
case SCREEN_STATUS_DEVICE_DETAILED:
checkPageDetailed();
break;
case SCREEN_ERROR:
errorPage();
break;
default:
break;
}
}
Azioni
Quando uno stato viene selezionato (cliccato), l’operazione corretta viene elaborata dalla relativa gestione dello stato, e lo SCREEN di ritorno mostra l’azione eseguita.
SCREEN clickMenu(SCREEN screen, int state){
SCREEN returningScreen = SCREEN_ERROR;
switch (screen) {
case SCREEN_MAIN:
returningScreen = mainMenuState((MENU_MAIN_STATE)state);
break;
case SCREEN_STATUS_DEVICE:
case SCREEN_STATUS_DEVICE_DETAILED:
case SCREEN_ERROR:
returningScreen = checkMenuState();
break;
default:
break;
}
encoderValue = 0;
screenEncoderSizeMax = screenEncoderSize[returningScreen];
SERIAL_DEBUG.print(F("CLICK --> "));
SERIAL_DEBUG.print( F("FROM SCREEN --> ") );
SERIAL_DEBUG.print( screen );
SERIAL_DEBUG.print(F(" - TO SCREEN --> "));
SERIAL_DEBUG.print( returningScreen );
SERIAL_DEBUG.print(F(" - MENU STATE --> "));
SERIAL_DEBUG.println( state );
return returningScreen;
}
Ecco le azioni al clic per la schermata principale
SCREEN mainMenuState(MENU_MAIN_STATE state){
SERIAL_DEBUG.print(F("mainMenuState --> "));
SERIAL_DEBUG.println(state);
ResponseStatus rs;
switch (state) {
case PUMP_START_MENU:
DEBUG_PRINTLN("PUMP_START_MENU");
stopPump();
operationalSelected = OPERATION_NORMAL;
SERIAL_DEBUG.println("OPERATION_NORMAL");
rs = sendPumpMessageToClient("start", true);
if (rs.code!=SUCCESS) {
errorMessage = rs.getResponseDescription();
return SCREEN_ERROR;
}
actionSelected = ACTION_AUTO;
return SCREEN_STATUS_DEVICE;
break;
case PUMP_START_PING_MENU:
DEBUG_PRINTLN("PUMP_START_PING_MENU");
stopPump();
operationalSelected = OPERATION_PING;
SERIAL_DEBUG.println("OPERATION_PING");
rs = sendPumpMessageToClient("start", true);
if (rs.code!=SUCCESS) {
errorMessage = rs.getResponseDescription();
return SCREEN_ERROR;
}
actionSelected = ACTION_AUTO;
return SCREEN_STATUS_DEVICE;
break;
case PUMP_STOP_MENU:
DEBUG_PRINTLN("PUMP_STOP_MENU");
stopPump();
actionSelected = ACTION_STOP;
operationalSelected = OPERATION_DISABLED;
rs = sendPumpMessageToClient("stopp", true);
if (rs.code!=SUCCESS) {
errorMessage = rs.getResponseDescription();
return SCREEN_ERROR;
}else{
return SCREEN_STATUS_DEVICE;
}
break;
case STATUS_MENU:
return SCREEN_STATUS_DEVICE;
break;
case STATUS_MENU_DETAILED:
return SCREEN_STATUS_DEVICE_DETAILED;
break;
default:
break;
}
return SCREEN_ERROR;
};
La funzione gestisce i messaggi sendPumpMessageToClient
ResponseStatus sendPumpMessageToClient(String action, bool wakeUp){
ResponseStatus rsW;
if (wakeUp){
rsW = setWakeUpMode();
}else{
rsW = setNormalMode();
}
SERIAL_DEBUG.println(rsW.getResponseDescription());
if (rsW.code != SUCCESS) return rsW;
JsonObject root = doc.to<JsonObject>();
root["type"] = action;
root["mode"] = (int)operationalSelected;
int size = measureJson(doc)+1;
char buf[size];
serializeJson(doc, buf, size);
SERIAL_DEBUG.println(buf);
SERIAL_DEBUG.println(measureJson(doc));
SERIAL_DEBUG.print("Send message to client ");
SERIAL_DEBUG.print(CLIENT_ADDH, DEC);
SERIAL_DEBUG.print(" ");
SERIAL_DEBUG.print(CLIENT_ADDL, DEC);
SERIAL_DEBUG.print(" ");
SERIAL_DEBUG.print(CLIENT_CHANNEL, HEX);
SERIAL_DEBUG.println(" ");
SERIAL_DEBUG.println("Mode --> ");
SERIAL_DEBUG.println(e32ttl.getMode());
rsW = e32ttl.sendFixedMessage(CLIENT_ADDH, CLIENT_ADDL, CLIENT_CHANNEL, buf, size);
SERIAL_DEBUG.println(rsW.getResponseDescription());
if (rsW.code != SUCCESS) return rsW;
rsW = setReceiveMode();
return rsW;
}
Grazie
Puoi trovare il codice master completo sul mio GitHub come al solito.
- Controllo remoto del livello della cisterna d’acqua e della pompa via LoRa: introduzione
- Controllo remoto del livello dell’acqua e della pompa via LoRa: server software
- Controllo remoto del livello dell’acqua e della pompa via LoRa: client software
- Controllo remoto del livello dell’acqua e della pompa via LoRa: PCB del server
- Controllo remoto del livello dell’acqua e della pompa via LoRa: PCB client
- Controllo remoto del livello dell’acqua e della pompa via LoRa: assemblare il server e box stampato in 3D
- Controllo remoto del livello dell’acqua e della pompa via LoRa: assemblare il client e box stampato in 3D