Estrapolare dati da un array char*

Buongiorno,
eccomi a chiedere supporto per poter estrarre e "manipolare" dei dati provenienti da un array mqtt.

Sostanzialmente un volta inizializzato il tutto (con libreria Pubsubclient) mi ritrovo con questa parte di codice:

void callback(char* topic, byte* payload, unsigned int length) {
  Serial.print("Message arrived [");
  Serial.print(topic);
  Serial.print("] ");
  for (int i = 0; i < length; i++) {
    Serial.print((char)payload[i]);
  }
  Serial.println();
}

Un nodeMCU è collegato via wifi alla mia rete e ad un raspberry che "sputa" in continuazione dati con i parametri del mio inverter. Purtroppo l'invio continuo (ogni secondo) della mole di informazioni provenienti dal raspberry mi rallentano poi broker ed altro dispositivo dove "leggo ed analizzo" i dati.

L'idea è quella di dare in pasto tutte queste informazioni ad nodeMCU ed inviare al mio broker informazioni ogni 10 - 30 secondi solo se ci sono variazioni (ad esempio, se il SOC batteria è 35%, non mi serve che venga inviato ogni secondo tale valore). Lato raspberry non ho possibilità di modifica, è un prodotto commerciale chiuso che continua a sputare dati:

15:31:21.812 -> Message arrived [solar_assistant/battery_1/cell_voltage_-_lowest/state] 3.246
15:31:21.812 -> Message arrived [solar_assistant/battery_1/current/state] -17.8
15:31:21.812 -> Message arrived [solar_assistant/battery_1/cycles/state] 72
15:31:21.812 -> Message arrived [solar_assistant/battery_1/power/state] -869
15:31:21.812 -> Message arrived [solar_assistant/battery_1/state_of_charge/state] 49.7
15:31:21.812 -> Message arrived [solar_assistant/battery_1/temperature/state] 21.7
15:31:21.812 -> Message arrived [solar_assistant/battery_1/temperature_env/state] 23.3
15:31:21.859 -> Message arrived [solar_assistant/battery_1/temperature_mos/state] 22.0
15:31:21.859 -> Message arrived [solar_assistant/battery_1/voltage/state] 48.8
15:31:21.859 -> Message arrived [solar_assistant/total/battery_power/state] -869
15:31:21.859 -> Message arrived [solar_assistant/total/battery_state_of_charge/state] 50

(Questa è solo una piccola parte di informazioni che continuano a fluire).

Ciò che vorrei fare, è qualcosa del genere:

void callback(char* topic, byte* payload, unsigned int length) {
if(topic == "solar_assistant/total/battery_state_of_charge/state" {
  Serial.print("SOC_batteria ");
  for (int i = 0; i < length; i++) {
  Serial.print((char)payload[i]);
  SOC_Batteria = payload[i];
  }
  Serial.println();
}

Mi rendo conto di essere in alto mare... sostanzialmente vorrei prendere i vari valore che ricevo (tensione batteria, tensione cella, tensione da pannelli ecc.) ed associare i relativi valori a delle variabili (come nell'esempio sopra per il soc batteria).
Una volta verificato che il valore non è uguale a quello precedente e se sono passati almeno 10 o 2 o x secondi, devo inviare nuovamente il tutto al broker.

Come dicevo per ora la mia difficoltà è "spacchettare" e ricavare i dati dall'array char.

Quel che mi fa strano, è che la parte di codice originale (quella del "message arrived..." mi spezza correttamente tutte le informazioni e me le stampa su seriale. Ecco, io vorrei invece che quei dati venissero copiati in variabili con relativo payload.

Grazie

I char array non puoi trattarli come stringhe intesi come oggetti. Quindi l'if che controlla se il topic è uguale ad una determinata stringa non lo puoi fare con l'== ma occorre utilizzare apposite funzioni.
Per imparare ad usare le stringhe classiche del C ... ovvero semplici array di char terminati dal carattere null (0x00) e le funzioni che trovi nella libreria standard (... che, oltretutto, è automaticamente inclusa dal IDE) AVR libc ed, in particolare, quanto è in <string.h> :wink:

strncpy
strtok
strlen
strncat

Quando hai capito come funzionano le stringhe classiche del C guarda questa funzione:
strncmp

1 Like

Grazie @fabpolli , mi avevi già dato una dritta tempo fa su una cosa simile...
Purtroppo non riesco a mettermi in testa sti array. O meglio, concettualmente ho capito di che si tratta ma fatico davvero tanto a comprendere come gestirli.

Oltre ai link che mi hai messo, hai una pagina dove viene spiegato tutto ciò ad un livello non troppo avanzato? Qualora non avessi sottomano i link, non preoccuparti, sarà mia premura cercare di capire il tutto.

Quello che mi fa strano, è vedere però la prima parte di codice dove semplicemente con un serial print mi scrive tutti i nomi dei parametri e relativi payload senza troppi sforzi, mi riferisco a questo:

void callback(char* topic, byte* payload, unsigned int length) {
Serial.print("Message arrived [");
Serial.print(topic);
Serial.print("] ");

Cioè, qua mi prende l'array dalla funzione, scrive banalmente "messaggio arrivato" e poi ci mette di fianco il topic senza dover tagliare / cercare il null ecc...)

Grazie per il supporto!

L'ultimo link che ti ho fornito è corredato da esempio, ma se vuoi qualcosa di più generico ho trovato questo con una rapida ricerca Google.
Il codice che menzioni che scrive "senza troppi sforzi" sulla seriale ti permette quel "lusso" solo perché qualcuno si è preso la briga di implementare nella classe Serial tutta una serie di metodi print che in base alla tipologia del parametro che viene passato alla funzione viene gestito e trattato, quindi la gestione del terminatore ecc. è demandato al codice della funzione, nulla di magico insomma :slight_smile: solo il lavoro di qualcuno che ti permette di ottenere quel livello di semplicità

Mi sembra, però, che l'if confronti il primo carattere... Coscienti di ciò, a volte si potrebbe sfruttarlo :slightly_smiling_face:

purtroppo quello che desideri fare non è per nulla semplice, per tre ragioni

  1. il nome della variabile è accessibile solo a compile-time, non c'è nessuna maniera "semplice" per conoscere o usare il nome di una variabile a run-time
  2. la gestionedegli array di caratteri terminati da 0 binario (queste sono le stringhe in C) è ittosto "farraginosa" se confrontata con altri linguaggi
  3. le "variabili" che ricevi sono solo degli array di byte (castati singolarmente a caratteri), quindi non sono ne un valore numerico ne una stringa di C (e nemmeno un oggetto String di C++, se è per questo)

però la cosa è "succosa" per chi come noi (io e il fratello) si fa chiamare "il dinamico duo"

quindi abbiamo studiato una "road-map" che ti permetterà di aggirare i vari problemi che ti ho elencato

dico aggirare perché purtroppo non sarà una passeggiata, ma almeno ci si avvicina

ti anticipo subito che abbiamo letteralmente saccheggiato il forum alla caccia di spunti e idee

cominciamo con il fatto che le variabili non hanno un nome gestibile a run-time
per simularlo serve di creare una struttura che conservi il "nome" della variabile e lo associ al suo valore
valore che deve essere un array di caratteri, come il "nome" della variabile

typedef struct
{

   char * nome;
   char valore[15];

} grandezza;

naturalmente per ogni "grandezza" in ricezione serve di inizializzare il suo "nome", che verrà conservato in un array

grandezza variabili[] = {{"solar_assistant/total/battery_state_of_charge/state"}};

e il suo valore deve essere più corto della dimensione definita nella typedef

adesso, conoscendo il numero delle variabili (e quindi la dimensione dell'array)
si può leggere nella callback il topic e confrontarlo coi nomi
appena si trova una variabile che ha il nome uguale al topic se ne aggiorna il valore
una cosa del tipo:

void callback(char * topic, byte * payload, unsigned int length)
{
   for (byte i = 0; ;)// vedi tu come terminare il costrutto
   {
      if (!strcmp(topic, variabili[i].nome))
      {
         //trovata
         for (int j = 1, j < length, j++)
         {
            variabili[i].valore[j] = (char) payload[j];
            variabili[i].valore[j + 1] = 0; // per assicurare che sia una stringa terminata

            if (j > 13)
            {
               break;
               // per assicurare che non si superi il limite di memoria assegnata}
            }
         }
         break;
         // trovata e non cerco oltre
      }
   }
}

a questo rimane il problema che le "variabili" hanno un nome non facile da ricordare, una cosa del tipo array[indice].valore

a questo si rimedia con il sapiente uso delle macro:

#define SOC_Batteria variabili[0].valore

che ci permette di usare nomi mnemonici invece di membri di array indicizzati

tutto questo lo abbiamo (lo ho) "rapinato" al forum

naturalmente a questo punto rimane solo di capire quando serve di trasmettere una variabile aggiornata

e questo si può fare ad esempio mantenendo nella struttura una doppia copia della variabile, una "vecchia" e una "nuova"
e nella callback eseguire un test con la strcmp()

questa secondo noi è "la strada" da seguire
perché mantiene l'integrità dei dati ed è facilmente espandibile

non credo
essendo le variabili confrontate dall'if dei puntatori a carattere l'if testa il valore del puntatore, quindi uguale se punta alla stessa arie di memoria, differente se punta ad una differente area di memoria, anche se questa contenesse gli stessi caratteri

Faccio fatica a crederlo, ma così è per cui ottimo lavoro davvero, ricavare questo codice dal forum è un lavorone. Diversamente io credo che il forum abbia fornito lo spunto, ora senza scendere nel dettaglio altro spunto potrebbe essere il seguente:

Ad un puntatore void è possibile assegnare qualunque puntatore, tuttavia per usare il puntatore occorre castarlo al giusto tipo. Al posto di char valore[15] ci potrebbe essere void *ptr_to;. Questo permette di associare un nome ad una variabile (globale o anche ad una string literal).

typedef struct
{

   char * nome;
   void * ptr_to;

} grandezza;

Il cast da char* verso void* è automatico, il cast da void* a char* deve essere esplicito. I vantaggi sono che:
associamo un nome ad una variabile c string usando un puntatore grande solo 16-bit (arch avr). Inoltre la c stringa puntata da ptr_to può essere di qualunque dimensione.

PS: Però si complica il codice per @khriss75.

Ciao.

Catalogo questo genere di stratagemmi nel cassetto "mala programmazione".
Utilizzare un comportamento limite del software, del compilatore, ecc. espone sempre a rischi che un domani la cosa non funzioni più, inoltre leggendo il codice a distanza di tempo o di altre persone che non sanno di una certa caratteristica nascosta vedono, nel caso più fortunato, un confronto errato (se sanno come funziona) tra stringhe e array di char. Nel caso sfortunato o comune invece credono che si possano confrontare array di char con stringhe.

1 Like

... più che "mala programmazione", come ha spiegato Ducembarr, è proprio concettualmente sbagliato ... :roll_eyes:

Guglielmo

Si concordo, rispondevo a Datman che indicava come possibile escamotage l'uso di qeull'if.
Che per gestire i char array sia indispensabile utilizzare le funzioni apposite ecc. è tassativo

Mi auguro di cuore che ciò che sto per scrivere non venga travisato da nessuno di voi che ha speso del tempo per rispondermi, quindi prima di tutto ancora GRAZIE!

Purtroppo vista le mie carenze, molte delle nozioni scritte sono troppo complesse da comprendere. Non dico che è colpa vostra, sia certo, è solo una mia mancanza ed incapacità.

Detto questo, la montagna insormontabile da gestire con codice mirabolante (concedetemelo ironicamente) trova soluzione con del "semplice" (per lo meno a me comprensibile) codice:

void callback(char* topic, byte* payload, unsigned int length) {

  String str = String((char*)topic);
  if (str == "solar_assistant/battery_1/state_of_charge/state") {
    String socco = String((char*)payload);
    float z = socco.toFloat();
    Serial.print("SOC batteria: ");
    Serial.println(z,1);
  }
  if (str == "solar_assistant/battery_1/cell_voltage_-_average/state") {
    String cell_voltage_average = String((char*)payload);
    cell_voltage_averagev = cell_voltage_average.toFloat();
    Serial.print("Tensione media delle celle batteria: ");
    Serial.println(cell_voltage_averagev,3);
  }
}

Il risultato è questo:

11:00:39.922 -> Tensione media delle celle batteria: 3.327
11:00:39.922 -> SOC batteria: 33.0
11:00:39.922 -> Tensione media delle celle batteria: 3.327
11:00:39.922 -> SOC batteria: 33.0
11:00:41.203 -> Tensione media delle celle batteria: 3.327
11:00:41.247 -> SOC batteria: 33.0
11:00:42.491 -> Tensione media delle celle batteria: 3.327
11:00:42.491 -> SOC batteria: 33.0

Sicuramente il codice non è ottimizzato, sicuramente c'è uno spreco immane di risorse ma effettivamente mi sta restituendo quanto ho bisogno.
Credo si possa trasformare il payload estratto direttamente in un float senza dover fare due passaggi.
I Serial.print andranno poi eliminati, mi servivano solo per verificare se i dati estratti sono corretti (e lo sono).
Avendo i valori dei vari payload (e ce ne saranno molti altri di parametri da gestire) associati ale relative variabili, sarà ora semplice verificare se il nuovo valore è differente rispetto a quello precedente ed inviarlo solo ogni x secondi (non mi serve un realtime).

Quello che non riesco a capire, è perché a fronte dei vostri consigli, la cosa si sia (forse) risolta semplicemente: assegno il "topic" ad una stringa, e lo comparo con il nome del parametro che mi interessa, assegno poi il payload ad un'altra stringa ed assegno tale valore (convertito in float) alla relativa variabile.
Ora continuerò ad aggiungere i vari "if" per prelevare i dati dai vari parametri che l'inverter butta fuori.

Se ci fosse un modo più veloce e "pulito" per fare questo

String cell_voltage_average = String((char*)payload);
    cell_voltage_averagev = cell_voltage_average.toFloat();

ottimizzerei il tutto (ora prendo il payload, lo scrivo in una stringa e poi lo converto nuovamente in float (a me ovviamente interessa subito il float).

Ringrazio nuovamente tutti per il supporto!

Cris

Per convertire in un unico colpo esisterebbe l'apposita funzione atof ad esempio, ma sostieni di non aver le capacità per comprendere l'uso delle funzioni legate agli array di char e quindi...
In ogni caso ho il forte dubbio che utilizzando l'oggetto String in quel modo esiste la possibilità, neanche troppo remota, che dopo un po' di funzionamento sperimenterai blocchi improvvisi e apparentemente senza senso. Saranno dovuti alla saturazione della memoria per via dell'allocazione dinamica dell'oggetto String.
Forse ti puoi salvare dichiarando una variabile globale di tipo String con dimensione sufficiente a contenere la massima lunghezza di topic e riutilizzarla per riassegnarvi i valori di topic ad ogni chiamata. Non uso mai la classe String visto che da miriadi di problemi quindi non so dire se anche riassegnando non si arrivi alla saturazione e corruzione della memoria con risultati catastrofici

Ci sono due strade ...

... la prima è preallocare un oggetto String di dimensioni massime sufficienti per il caso peggiore con l'apposito metodo reserve(). Questo evita il dover in continuazione allocare e liberare memoria con le conseguenze che ben conosciamo.

... la seconda è utilizzare la classe SafeString che è studiata apposta per evitare i noti problemi della classe String. Bisogna dedicarci qualche minuto a studiarla, ma è una valida alternativa.

Guglielmo

Il mio dubbio era su

String((char*)payload)

che anche se assegnato poi alla globale non provochi il solito problema di memoria visto che alloca comunque un oggetto String ad ogni chiamata.

Stai lavorando con un NodeMCU (presumo ESP8266), hai tutta la SRAM necessaria per concederti "serenamente" le comodità della classe String.

Inoltre hai giustamente usato delle variabili locali che vengono deallocate in automatico quando la funzione termina quindi non vedo proprio alcun problema.

L'unica cosa che mi viene in mente è il casting è del tutto superfluo: cosi funziona lo stesso (ci pensa ma classe a gestire l'input).

String str = topic;

Ma con atof non è semplicissimo?...
https://cplusplus.com/reference/cstdlib/atof/

Grazie, ricevo volentieri di questi complimenti

si e no
perchè non si conosce il tipo della variabile in ricezione e quindi nemmeno quanto spazio allocare
un puntatore in una struttura non alloca area di memoria

Non mi lascia quotare non so perché

Comunque no, non è semplicissimo
Anzi è pericoloso

Perché payload non è una stringa, ma un array di byte, nulla garantisce che sia terminato da zero binario, a nulla vale castare il suo puntatore, i dati non cambiano

Il mio dubbio invece è che come dice Ducembarr (che è personaggio che viene dell'impero galattico sbagliato) payload non è una stringa di C, e anche in questo caso la conversione è non sicura, rischia di impiantare la MCU

1 Like