manipolazione stringa [RISOLTO]

Buongiorno a tutti.

Dunque devo aggiornare un lavoro di qualche hanno fa. In pratica tramite lo smartphone devo inviare un comando via bluetooth ad arduino. Il comando contiene un messaggio alfanumerico che ha come intento quello di programmare un orario di accensione e uno di spegnimento per ogni singolo giorno. Quindi ho pensato ad una cosa del genere:

L1830-1230

  • dove L1 sta per lunedi fascia 1;
  • 830 per l'ora di accensione;
  • 1230 per l'ora di spegnimento;
//esempio messaggio inviato tramite bluetooth: L1830-1230


String message = Serial.read();  // leggo dalla seriale 
message.toUpperCase();           // rendo maiuscola tutta la stringa per evitare errori di sintassi

if (message.startsWith(L1)) {   // controllo se nella stringa c'è L1
    
  // vorrei eliminare L1 per ottenere solo 830-1230
  // poi vorrei splittare la stringa attorno al carattere - per ottenere due tronconi 830 e 1230
  // e infine memorizzare i due tronconi in due variabili di tipo int ad esempio int L1ON; e int L1OFF; 

}

non avendo mai lavorato con le stringhe, mi aiutate a capire come effettuare i 3 comandi commentati?

Grazie

Ciao! Io ti consiglierei di usare le stringhe stile C ovvero array di char! Premettiamo che il tuo codice legge un carattere da seriale e non una stringa :slight_smile: come stato detto su un microcontrollore non ci sono meccanismi per liberare la memoria per cui è meglio non usare un oggetto String.

  1. Leggiamo la stringa, ci sono esempi come leggere una stringa da seriale "usando stringhe del C"

  2. Se la stringa non varia di lunghezza, esempio abbiamo sempre due caratteri iniziali, e poi abbiamo le ore espresse sempre con 4 caratteri, è molto facile separare la stringa in sottostringhe, perché sappiamo bene dove iniziano e dove finiscono :wink:

Esempio:

/* ricevo da seriale la stringa L10830-1230 , se l'ora è minore di 10 metto uno zero davanti, salvata in un array di char stringa[12] */

char stringa[12];
int oraOn=0;
int minutiOn;

int oraOff=0;
int minutiOff=0;

char comando[4]={0};
char tmp[10];

void loop(){

/* leggo la stringa da seriale e la inserisco nell' array stringa "trovi esempi" */


strncpy(comando,stringa,2); // In comando è presente la stringa L1
strncpy(tmp,stringa+2,2); 
// Passo il puntatore al 2 carattere di stringa, e prelevo 2 caratteri, tmp= 08

oreOn=atoi(tmp);

strncpy(tmp,stringa+4,2); // tmp=30
minutiOn=atoi(tmp);

strncpy(tmp,stringa+7,2); // tmp=12
oreOff=atoi(tmp);
strncpy(tmp,stringa+9,2); // tmp=30

minutiOff=atoi(tmp);

if(strcmp(comando,"L1")==0) // Se comando è uguale a L1 faccio qualcosa

 
}

Ciao, mai mai mai usare la classe String su Arduino, all'inizio sembra una figata poi quando si pianta tutto all'improvviso a casaccio e senza capire il perché diventa un incubo. Segui il consiglio e usa gli array di char.
Altra cosa il tuo protocollo di comunicazione ha due leggerissimi problemi:
Il primo prevedi un solo carattere per il giorno L = Lunedì, M =Martedì, M = Mercoledì... oooops! usa anzi il formato numerico che con un carattere te la cavi (1 = Lunedì, 2 = martedì, 3 = mercoledì, ecc.)
Il secondo è che essendo formattata in quel modo se invece di accendere alle 8:30 accedi alle 11:22 ti aumenta di un carattere e ti si scombinano tutte le posizioni, quindi se devidi di avere stringhe di lunghezza fissa per splittare la stringa in base alla posizione allora devi prevedere di formattari gli orari in modo che occupino sempre 4 posizioni (Es 0830, 1101, 1547) oppure cambi approccio e usi sempre dei separatori, la stringa sarà più lunga ma saprai sempre con certezza l'informazione ste sta trattando, esempio estremi separando ogni possibile valore

L;1;8;30;18;44

usando le funzioni strtok e simili separarai la stringa facilmente, dopodiché con le alre funzioni (atoi e simili) tradurrai le parti nei rispettivi valori dentro variabili adatte allo scopo
chiaro che con la spearazione indicata sopra hai la massima flessibilità e la massima lunghezza della stringa, puoi adottare ance vie di mezzo

L1;8;30;18;44
L1;830;1844

Se non hai problemi a inviare tanti caratteri la prima forma è sicuramente la più flessibile e gestibile, poi vedi tu

fabpolli:
Il primo prevedi un solo carattere per il giorno L = Lunedì, M =Martedì, M = Mercoledì... oooops! usa anzi il formato numerico che con un carattere te la cavi (1 = Lunedì, 2 = martedì, 3 = mercoledì, ecc.)

Fai 1= Domenica, 2= Lunedì, 3= Martedì, ecc. ecc. ... così sei compatibile con le informazioni che di default restituiscono gli RTC (DS1307, DS3231, ...). :wink:

Guglielmo

Grazie ragazzi, mi metto all'opera, e provo le vai soluzioni.

Grazie ancora

fabpolli:
quindi se devidi di avere stringhe di lunghezza fissa per splittare la stringa in base alla posizione allora devi prevedere di formattari gli orari in modo che occupino sempre 4 posizioni (Es 0830, 1101, 1547) oppure cambi approccio e usi sempre dei separatori, la stringa sarà più lunga ma saprai sempre con certezza l'informazione ste sta trattando, esempio estremi separando ogni possibile valore

Come dico spesso (ed ultimamente non so perché ma lo sto riscontrando spesso..:wink: ) se è una comunicazione tra MACCHINE è sempre bene usare una codifica per MACCHINE, possibilmente rendendo la cosa più semplice per un processorino come quello di Arduino...

droidprova:
Grazie ragazzi, mi metto all'opera, e provo le vai soluzioni.

Ma prima devi ben definire cosa vuoi comunicare, e poi disegnare il protocollo. Ti faccio un esempio.

Se tu vuoi mandare una informazione relativa al giorno della settimana (tralascio per ora il discorso della "fascia" che non ho capito) e due orari rispettivamente di accensione e spegnimento, io farei un pacchetto di lunghezza fissa contenente queste informazioni. Del tipo:
gaaaassss
dove "g" è il codice del giorno (come ha detto Guglielmo, 0 è Domenica, 1 è Lunedì e così via), "aaaa" è l'ora di accensione nel formato HHMM, e "ssss" quello di spegnimento.
Questa è una comunicazione stringa (non "String" mi raccomando, usa un "char buffer[]" dimensionato al numero massimo di caratteri del protocollo, più uno) di dimensione fissa. Potresti anche compattare il tutto invece di 4 caratteri usando 2 byte per ogni orario (il primo byte avrà valore pari a quello dell'ora, il secondo dei minuti, oppure dato che i minuti in un giorno sono 1440 ti basta un unsigned int quindi sempre 2 byte). Ma per ora parliamo solo di caratteri per semplicità.

Questa cosa, ad esempio, l'avevo progettata per un prototipo di centralina di irrigazione a 4 canali (quindi nel mio caso il formato era "cgaaaassss" dove si aggiungeva "c" col codice del canale 1-4) anche se non come comando ma nel file di configurazione dei tempi e zone di irrigazione.

Ora, nel canale tu mandi solo questo tipo di informazioni, ossia "il giorno G accendi alle XX:XX e spegni alle YY:YY"? Non so quale sia la tua applicazione pratica, ma se parlassimo di luci o apparati generici, per i quali potrei avere l'accensione alle 22 e lo spegnimento alle 6 di mattina? E in questo altro canale è possibile che (ora o magari per future espansioni) ci passino anche altre informazioni differenti da "accendi questa cosa dalle X alle Y"?

Per cui un protocollo più generico potrebbe prevedere un primo carattere che identifica il tipo di messaggio/comando, seguito dal suo "payload" ossia il contenuto del messaggio.
Ad esempio, indicando con "L"(fisso)= comando "Luce", poi "c"=una cifra 0-9 per il canale (o fascia?), "A"(fisso)=comando ACCENDI oppure "S"(fisso)=comando SPEGNI, "g"=giorno della settimana, e "hhhh"=ora avremo:

LcAghhhh
LcSghhhh

ad esempio:

L1A20830 // Accendi il canale 1 alle 08:30 del Lunedì
L1S21830 // Spegni il canale 1 alle 18:30 del Lunedì
L2A32030 // Accendi il canale 2 alle 20:30 del Martedì
L2S40630 // Spegni il canale 2 alle 06:30 del Mercoledì

In futuro potrai anche mandare sullo stesso canale altri comandi, ad esempio "Imposta il termostato 2 a 26 gradi":

T226

Nel ciclo principale di lettura delle comunicazioni andrai dapprima a leggere il primo carattere che indica il tipo di messaggio, poi se è una "L" leggerai i successivi 7 caratteri, se è "T" i successivi 3, e così via.

docdoc:
dove "g" è il codice del giorno (come ha detto Guglielmo, 0 è Domenica, 1 è Lunedì e così via), "aaaa" è l'ora di accensione nel formato HHMM, e "ssss" quello di spegnimento.

mmmm ... se guardi il datasheet dei vari RTC ti accorgi che partono da 1 che è Domenica (campo Day 1..7) :wink:

Guglielmo

la settimana americana :smiley:

gpb01:
mmmm ... se guardi il datasheet dei vari RTC ti accorgi che partono da 1 che è Domenica (campo Day 1..7) :wink:

Si, intendevo quello, ho scritto male (d'altronde lo avevi scritto...:wink: ) quindi 1=Domenica, eccetera.

docdoc:
Come dico spesso (ed ultimamente non so perché ma lo sto riscontrando spesso..:wink: ) se è una comunicazione tra MACCHINE è sempre bene usare una codifica per MACCHINE, possibilmente rendendo la cosa più semplice per un processorino come quello di Arduino...

E qui mi trovi d'accordo ma credo, e correggimi se sbaglio, usare un tokenizer per suddividere la stringa sia più efficente che non andare per posizioni, oltretutto per future espansioni non ti leghi a un protocollo fisso ma puoi adattarti in base al messaggio in arrivo. Se parlassimo di messaggi fissi da separare in parti da un carattere ciascuno (Es. L1A, L1S) allora sarei d'accordo andare su posizione fissa ben precisa confrontabile (A[2]=='A') è più efficente e veloce del tokenizer.
Inoltre considerando un solo valore come posizione per il dispositivo, secondo me, va bene se e solo se non sarà mai possibile andare oltre i 10 dispositivi altrimenti conviene andare in doppia cifra allungando di fatto il messaggio.

fabpolli:
E qui mi trovi d'accordo ma credo, e correggimi se sbaglio, usare un tokenizer per suddividere la stringa sia più efficente che non andare per posizioni

Beh può essere più leggibile per un umano (ma dalle premesse non essendo informazioni che devi interpretare "ad occhio" mi pare abbastanza inutile), ma efficiente no di sicuro (tokenizzando prima devi CERCARE la posizione del token e poi ESTRARRE il dato, se il messaggio ha una lunghezza predefinita fai solo la seconda parte di estrazione).

oltretutto per future espansioni non ti leghi a un protocollo fisso ma puoi adattarti in base al messaggio in arrivo. Se parlassimo di messaggi fissi da separare in parti da un carattere ciascuno (Es. L1A, L1S) allora sarei d'accordo andare su posizione fissa ben precisa confrontabile (A[2]=='A') è più efficente e veloce del tokenizer.

Ma la lunghezza del payload la determini in base al primo byte che identifica il tipo di pacchetto. Puoi avere benissimo un protocollo nel quale il primo byte indica il messaggio specifico, seguito da un numero variabile di byte (o con un qualcosa che ne determina la lunghezza, come il primo byte per messaggi max 255 caratteri, oppure fino ad un terminatore come un byte 0x00). Tra l'altro nel mio esempio ho fatto anche il caso del termostato ("T226") il cui payload è di 3 byte invece di 7...

Inoltre considerando un solo valore come posizione per il dispositivo, secondo me, va bene se e solo se non sarà mai possibile andare oltre i 10 dispositivi altrimenti conviene andare in doppia cifra allungando di fatto il messaggio.

Per quello dico che PRIMA si deve capire cosa si vuol fare, e quali potrebbero essere le possibili prossime espansioni, e POI si progetta il protocollo.
Nel caso che poni ad esempio, parliamo di un carattere ma se lo si trasforma in byte ecco che hai 256 device. Non bastano? Metti un unsigned int da 2 byte e stai a posto da qui all'eternità... :slight_smile:

@docdoc si in effetti non avevo riflettuto sul doppio passaggio, mi sono basato sulla facilità d'iterazione sulla stringa rispetto a prelevarne direttamente pezzi ben definiti (come posizione e dimensioni).
Trasformaare il carattere in byte è più che chiaro (almeno per me) ma di difficile comprensione a chi si approccia all'inizio alla trasmissione dei dati (e ultimamene sul forum ci sono molti topic a tal riguardo) è più semplice pensare "a carattere" piuttosto che a numero.
Grazie del chiarimento sull'efficienza :slight_smile:

fabpolli:
Trasformaare il carattere in byte è più che chiaro (almeno per me) ma di difficile comprensione a chi si approccia all'inizio alla trasmissione dei dati (e ultimamene sul forum ci sono molti topic a tal riguardo) è più semplice pensare "a carattere" piuttosto che a numero.

Si, come detto è più "leggibile" in debug, ma in ogni caso basta dire di usare write() invece di print() ed il gioco è (quasi) fatto.. :wink:

Ok ho letto tutto, grzie 1000 per gli interventi. Siccome non ho mai lavorato con le stringhe e vorrei imparare, non capisco perchè tutti siete d'accordo che sia meglio un char rispetto ad una String. Me lo spiegate in parole povere?

In ogni caso ho cercato di fare quello che avevo cominciato, ossia adoperate le String, in quanto le studiavo da un pò, e tutto funziona, nel senso che riesco ad estrarre i dati che mi servono. Lo so per certo perchè ho debbuggato con la serialprint.

I dati li invio/ricevo dallo smartphone usando un bot di telegram e poi manipolo la stringa direttamente nella sua funzione void Bot_ReplyMessages(). Il bot è ok in quanto i comandi semplici di on e off e get temp vanno alla grande e da molto tempo.

Vi mostro il codice:

.......
// variabili accensioni wi-fi
bool man;    // modalità manuale crono
bool aut;    // modalità auto crono
float t_set; // temperatura letta dal sensore
float t_h = 0.1;  // temperatura isteresi impianto
int LU1ORAON;
int LU1MINON;
int LU1ORAOFF;
int LU1MINOFF;
int MA1ORAON;
int MA1MINON;
int MA1ORAOFF;
int MA1MINOFF;
int ME1ORAON;
int ME1MINON;
int ME1ORAOFF;
int ME1MINOFF;

void setup (){

...

}

void loop() {

...
accensioni();

}

void Bot_ReplyMessages() {

  for (int i = 1; i < bot.message[0][0].toInt() + 1; i++) {
    String chatID = bot.message[i][4];
    String message = bot.message[i][5];
    message.toUpperCase();                         // rendo maiuscola tutta la stringa per evitare errori di sintassi
    String reply;

  if (message == "ON") {                         // accende il carico
      digitalWrite(rele, HIGH);
      state = true;
      reply = "acceso";
    }

    else if (message == "OFF") {                   // spegne il carico
      digitalWrite(rele, LOW);
      state = false;
      reply = "spento";
      man = false;
      aut = false;
    }

   else if (message.startsWith("AUTO")) {         // controllo se nella stringa AUTO20 c'è auto
      aut = true;                                  // attiva modalità auto
      t_set = message.substring(4, 8);    // imposta soglia temperatura
      reply = "temperatura impostata in modalità auto";
    }

    else if (message.startsWith("LU1")) {          // controllo se nella stringa LU10830-1230 c'è LU1
      String(LU1ORAON) = message.substring(3, 5);  // creo una sottostringa con il valore di ora ON
      String(LU1MINON) = message.substring(5, 7);  // creo una sottostringa con il valore di min ON
      String(LU1ORAOFF) = message.substring(8, 10); // creo una sottostringa con il valore di ora OFF
      String(LU1MINOFF) = message.substring(10, 12); // creo una sottostringa con il valore di min OFF
      LU1ORAON = message.substring(3, 5);
      LU1MINON = message.substring(5, 7);
      LU1ORAOFF = message.substring(8, 10);
      LU1MINOFF = message.substring(10, 12);
      reply = "fascia oraria lunedì/1 memorizzata";
 
   .......// prosegue con gli altri else if per gli altri giorni

    }

le variabili int globali poi le passo alla funzione accensioni:

void accensioni() {
  DateTime now = getLocalTime();

  Serial.println(t_set);
  // provo a stampare le variabili di mercoledì
  Serial.println(ME1ORAON);
  Serial.println(ME1MINON);
  Serial.println(ME1ORAOFF);
  Serial.println(ME1MINOFF);


  if (man == true) {  // condizione modalità manuale
  if (t_read < t_set - t_h) {
      digitalWrite(rele, HIGH);
    } else if (t_read > t_set + t_h) {
      digitalWrite(rele, LOW);
    }
  }



  if (aut == true)   { // condizione modalità automatica

  if (now.day()  == 2) {  // condizione giorno lunedì'
      if ((LU1ORAON == now.hour()) & (LU1MINON == now.minute())) {
        if ((t_read < t_set - t_h)) {
          digitalWrite(rele, HIGH);
        } else if (t_read > t_set + t_h) {
          digitalWrite(rele, LOW);
        }
      }
      if ((LU1ORAOFF == now.hour()) & (LU1MINOFF == now.minute())) {
        digitalWrite(rele, LOW);
      }
    }

    if (now.day()  == 3) {   // condizione giorno martedì
      if ((MA1ORAON == now.hour()) & (MA1MINON == now.minute())) {
        if ((t_read < t_set - t_h)) {
          digitalWrite(rele, HIGH);
        } else if (t_read > t_set + t_h) {
          digitalWrite(rele, LOW);
        }
      }
      if ((MA1ORAOFF == now.hour()) & (MA1MINOFF == now.minute())) {
        digitalWrite(rele, LOW);
      }
    }


    if (now.day()  == 4) { // condizione giorno mercoledì
      if ((ME1ORAON == now.hour()) & (ME1MINON == now.minute())) {
        if ((t_read < t_set - t_h)) {
          digitalWrite(rele, HIGH);
        } else if (t_read > t_set + t_h) {
          digitalWrite(rele, LOW);
        }
      }
      if ((ME1ORAOFF == now.hour()) & (ME1MINOFF == now.minute())) {
        digitalWrite(rele, LOW);
      }
    }
  }
}

ma dette variabili risultano sempre vuote. Perchè, non capisco, mi sembra tutto ok.

droidprova:
Siccome non ho mai lavorato con le stringhe e vorrei imparare, non capisco perchè tutti siete d'accordo che sia meglio un char rispetto ad una String. Me lo spiegate in parole povere?

Un vettore di char (stringhe classische del C) viene allocato e deallocato dalla memoria al pari di qualsiasi altra variabile, sia essa int, byte, float, ecc. con tutte le ottimizzazioni del caso da parte del compilatore (variabili inutilizzate non vengono considerate, ecc. ecc.)
Usando la classe String invece le variabili vengono allocate dinamicamente in memoria ma quando queste cesano di esistere la memoria non viene correttamente liberata, durante l'esecuzione del codice se quest'operazione avviene molte volte la memoria via via viene occupata sempre di più fino ad arrivare a saturazione con conseguente comportamento anomalo del programma. Questo avviene perché su piccole MCU non esiste (per via delle ridotte risorse hardware) un garbage collector che si occupa di analizzare la memoria e ripilirla da "oggetti" che non servono più. Solitamente non si riscontra questo genere di problemi su PC poiché su tale dispositivo è presente appunto il garbage collector che si coccupa di fare questo lavoro.

Wow. Grazie 1000. Adesso il quadro è più chiaro.

Hai idea del perchè i valori assegnati alle variabili non vengono scambiati tra le due funzioni? E' un problema di approccio al codice o appunto di aver utilizzato la classe String?

Grazie ancora

droidprova:
Vi mostro il codice:

.......

int LU1ORAON;

String(LU1ORAON) = message.substring(3, 5);  // creo una sottostringa con il valore di ora ON

LU1ORAON = message.substring(3, 5);




Perchè, non capisco, mi sembra tutto ok.

Scrivi, a parole, cosa fanno queste tre righe
Il perché ti sarà chiaro

l'intento è dichiarare una variabile globale di tipo int che poi dovrà assumere il valore di una sottostringa ottenuta a sua volta manipolando la stringa madre e passare la variabile int cosi ottenuta ad una funzione che la compari all'ora di un RTC per accendere poi un relè...

L'intento lo sapevamo
È "cosa" fanno" quelle righe, che devi investigare
Leggiti magari il reference....

la prima come da reference, converte la su.string in int. -> stampo la variabile ed è piena

la seconda riga l'ho aggiunta un pò per prova -> stampo la variabile ed è piena

la passo alla funzione accensioni() e si svuotano.