Feb 212016
 

In questo articolo vediamo come preparare un “protocollo” per condividere dati fra più dispositivi, indipendentemente che questi siano degli Arduino, Raspberry, PC o quant’altro anche se userò Arduino come base per questo testo. Scrivo questo articolo perché oramai sono parecchie le richieste di questo genere, in molti mi scrivono chiedendomi aiuto su come inviare dati fra un dispositivo e un altro. Sono dell’idea che la maggior parte delle persone non riesce nell’impresa per un semplice motivo: non conosce il linguaggio C e con questo limite di fondo diventa davvero difficile costruire qualcosa di funzionante ed efficiente. Guarda caso ho appena completato un nuovo lavoro, ho ripreso il libro Easy C che da anni è disponibile gratuitamente sul blog, l’ho sottoposto ad una lunga revisione, aggiornato, corretto e re impaginato in formato epub3 e da pochissimo è disponibile su Google Play. Certo non è gratuito, ma se trovate un libro migliore allo stesso prezzo mi faccio monaco. Inteso però che è un libro per chi vuole cominciare da zero.

Il problema

Prima di trovare delle soluzioni dobbiamo come sempre capire qual’è il problema. La nostra intenzione è forse creare un protocollo di comunicazione fra dispositivi, un protocollo che possa funzionare tanto per una RS485 che via WiFi o attraverso gli nrf24? Ci sono tante domande a cui rispondere, ad esempio quali dati vogliamo inviare e ricevere?, qual’è il livello di affidabilità e sicurezza delle trasmissioni? Ad esempio sappiamo che gli nrf24 in caso di errore di trasmissione re inviano automaticamente i pacchetti di informazioni finché la trasmissione è avvenuta correttamente, ma ciò non si verifica ad esempio sulla rs485. Ci sono casi in cui le trasmissioni devono essere cifrate per non essere comprensibili nemmeno se intercettate, ma a questo punto diventa importante la potenza dell’hardware a disposizione. Abbiamo preso in considerazione quale deve essere la velocità di trasmissione e con quale velocità verranno elaborate le informazioni? La connessione è fra un master e più slave o è multimaster? Il protocollo deve gestire eventuali errori sulle informazioni ricevute? I pacchetti di dati devono avere tutti la stessa lunghezza o sono variabili?

Come vedete la quantità di domande che possiamo porci è davvero infinita per cui non è possibile fornire una risposta univoca che possa valere per tutti gli ambiti applicativi. Inoltre questo articolo è indirizzato a chi non è in grado di costruirsi da se un protocollo di comunicazione per cui per forza di cose dobbiamo limitarci a qualcosa di semplice ma che possa essere utilizzato in numerose situazioni diverse.

Non inventiamo l’acqua calda

Quasi sempre la soluzione migliore non è quella di inventarsi qualcosa di nuovo da zero ma è meglio ispirarsi a quanto già esiste in ambito informatico. Ho deciso perciò di prendere spunto dai formati TIFF e DICOM (un protocollo per la trasmissione di immagini medicali). Vediamo qui di seguito la possibile semplificazione del protocollo di comunicazione:

@ S D G E L x x x ^

Nella tabella qui sopra potete vedere la schematizzazione del protocollo che ho pensato di proporvi. Le lettere riportate hanno i seguenti significati:

@: byte di inizio trasmissione.
S: Sorgente: indirizzo del dispositivo trasmittente
D: Indirizzo del dispositivo di destinazione
G: Gruppo
E: Elemento
L: Lunghezza
xxx: Dati
^: Byte di fine trasmissione

I byte di inizio e fine trasmissione indicano appunto il primo e l’ultimo byte della trasmissione permettendo ai riceventi di avere dei chiari riferimenti.

S rappresenta l’indirizzo del dispositivo che trasmette, essendo un byte è ovvio che ci potranno essere un massimo di 256 dispositivi. Spesso nei protocolli di trasmissione vengono però riservati alcuni indirizzi per funzioni speciali, ad esempio per l’invio di dati a tutti i dispositivi, messaggi di debug, ma possono essere assegnati degli indirizzi anche per dispositivi che svolgono particolari funzioni all’interno di una rete. Lo stesso discorso vale perciò per D che è l’indirizzo di destinazione. Faccio notare che gli indirizzi sono posti all’inizio, in questo modo il destinatario se intercetta un messaggio non diretto a lui, può del tutto ignorare il contenuto dei dati successivi sino al byte di fine trasmissione, anche se tale opzione non sarà presa in considerazione nei listati che seguiranno.

Con G ed E, ossia gruppo ed elemento, intendiamo la classificazione del dato che stiamo inviando. Cerco di spiegarmi meglio. Il nostro protocollo a priori non conosce la tipologia di dati che siamo intenzionati ad inviare, potrebbe trattarsi di temperature lette da un sensore, o correnti, tensioni, distanze , lo stato di pressione di un pulsante o qualunque altra cosa ci venga in mente. Bene, il protocollo così strutturato mette a disposizione 256 raggruppamenti e per ognuno di essi 256 possibili sotto categorie per un totale di 65536 diverse possibilità. Ad esempio potremmo stabilire che il gruppo zero trasporta messaggi di allerta, che il gruppo 1 trasporta i messaggi di debug, che il due trasporta letture da sensori, etc etc. Facciamo per un attimo finta di usare il gruppo 1 per l’invio delle letture dei sensori, il sottogruppo potrebbe usare lo 0 per le temperature, l’1 per l’umidità, il 2 per la pressione, etc, etc.

Con L si intende la lunghezza del pacchetto dati che segue, ciò permette di inviare pacchetti di dati da 1 a 256 byte. Chiaramente in questo esempio stiamo predisponendo un protocollo adatto principalmente ai microcontrollori, ma se volessimo ampliare le cose e magari trasferire intere immagini con un PC, allora il protocollo dovrà prevedere 2,3 o anche 4 bytes per memorizzare la dimensione del pacchetto dati. Ci sono protocolli come lo stesso DICOM che prevedono la possibilità di spezzettare la trasmissione dei dati in piccoli pacchetti che poi sono riuniti dal ricevente, in questo modo in caso di errori sarà possibile re inviare solo il pacchetto malformato. In questo articolo non oseremo tanto, ci limitiamo ad inviare e ricevere tutto in un blocco unico. Dopo la lunghezza segue il pacchetto dati vero e proprio e per finire il byte di chiusura trasmissione.

Da dove cominciamo?

La prima cosa da pensare è: usiamo il C o il C++? Strutture o classi? In linea di massima diciamo che il C++ con le classi ci permette di scrivere codice più facilmente riutilizzabile ed integrabile in altri progetti, il C invece ci permette mediamente di creare codice maggiormente ottimizzato e più piccolo, cosa da non sottovalutare con Arduino UNO che di base ha davvero poche risorse a disposizione. Assodato che gran parte dei progetti Arduino sono scritti in C, perlomeno i piccoli esempi che troviamo quotidianamente in rete, adotterò anch’io questa strategia, magari se l’articolo suscita interesse posso sempre pensare di scrivere una seconda parte con la “conversione” in C++.

Struttura base e trasmissione

La cosa più logica da fare è creare una struttura che rappresenti il protocollo che abbiamo deciso di scrivere. Ah, dobbiamo decidere un nome per questo protocollo: che ne dite di MMP, McMajan Protocol

struct MMP
{
 unsigned char sorgente;
 unsigned char destinazione;
 unsigned char gruppo;
 unsigned char elemento;
 unsigned char lunghezza;
 unsigned char * buffer;
}

Sicuramente sarà necessaria una funzione per inizializzare tutti i parametri della struttura nel momento in cui ci prepariamo ad effettuare una trasmissione, questo tipo di funzione semplifica anche l’eventuale passaggio al C++ (costruttore, vi dice nulla?).

struct MMP * MMP_New()
{
 struct MMP * myMMP;
 myMMP=(struct MMP*)malloc(sizeof(struct MMP));
 if(!myMMP) return(NULL);
 myMMP->sorgente=0;
 myMMP->destinazione=0;
 myMMP->gruppo=0;
 myMMP->elemento=0;
 myMMP->lunghezza=0;
 myMMP->buffer=NULL;
 return(myMMP);
}

Grazie a questa funzione possiamo inizializzare una struttura MMP quasi come con le classi del C++, ci basterà infatti una singola istruzione del tipo:

struct MMP * mia_MMP=MMP_New();

Senza aggiungere funzioni apposite possiamo permetterci il lusso di fissare in modo diretto sorgente e destinazione, ad esempio potremmo scrivere mia_MMP->sorgente=15; , giusto per fare un esempio. 

Un po’ più complesso è invece il settaggio del buffer in quanto dipende molto da che dati vogliamo inviare e da come li vogliamo codificare. Dato che voglio portarvi un esempio concreto stabiliamo per esempio che useremo il protocollo per trasmettere dati di temperatura (gruppo 1, elemento 1), di umidità (guppo 1, elemento 2) e stato di pressione di pulsanti esterni (gruppo 2, elemento 1).

Per trasmettere i dati di temperatura ed umidità utilizziamo dei float per cui ci occorrono 4 bytes. Ma non basta perchè oltre al dato di temperatura ed umidità veri e propri, dobbiamo in qualche modo identificare il sensore di partenza, infatti l’Arduino connesso allo specifico modulo nrf24 potrebbe gestire da solo un centinaio di sensori. A questo punto ci si aprono due strade. O utilizziamo l’indirizzo di spedizione per identificare ogni singolo sensore e non solamente il dispositivo che li controlla, oppure all’interno dei dati dobbiamo usare una codifica per l’identificazione del sensore. Direi che questa seconda strada sia la migliore perché offre una maggior elasticità, anche se comporta un discreto incremento di complessità. Riassumendo, per ogni sensore di temperatura ed umidità, ci servono 4 byte per memorizzare il float ed uno per identificare il numero di sensore. Invece per lo stato di pressione dei pulsanti ci servirà un byte per l’identificativo del pulsante stesso ed uno per lo stato di pressione o rilascio.

Ma concentriamoci sulla temperatura. Abbiamo detto che fissiamo gruppo ed elemento ad uno, possiamo farlo in modo diretto con:

 mia_MMP->gruppo=1;
 mia_MMP->elemento=1;

Per semplificare ho preparato anche la funzione MMP_PrepareGE per cui possiamo semplificare con MMP_PrepareGE(mia_MMP,1,1). Resta da preparare il buffer con i dati, un primo byte per l’identificativo del sensore e altri 4 per contenere il float. A questo punto ci sono due vie, una che prevede la scrittura di una funzione apposita per ogni tipo di dato che potremmo decidere di “impachettare”, ma la nostra scelta ricadrà su una soluzione più efficiente basata sull’uso dei puntatori e che prevede l’uso di una sola funzione per qualsiasi tipo di dato. E’ inoltre prevista la possibilità di inviare messaggi senza alcun dato, ad esempio se dobbiamo segnalare ad una scheda remota che ci siamo appena riavviati, basteranno gruppo ed elemento senza  alcun dato da inviare.

unsigned char MMP_SetData(struct MMP *myMMP,void * data,unsigned char ln, unsigned char id)
{
  unsigned char datalen=ln+1;
  unsigned char i;
  if(myMMP->lunghezza!=datalen)
  {
   myMMP->lunghezza=datalen;
   if(datalen==0) free(myMMP->buffer);
   else
   {
     unsigned char * localbuffer;
     localbuffer=(unsigned char*)realloc(myMMP->buffer,datalen);
     if(!localbuffer) {
       myMMP->lunghezza=0;
       return(0);
     }
     myMMP->buffer=localbuffer;
   }
  }
  myMMP->buffer[0]=id;
  for(i=0;i<ln;i++) *((unsigned char*)myMMP->buffer+1+i)=*((unsigned char*)data+i);
  return(1);
}

Per preparare l’invio della nostra temperatura sarà sufficiente una chiamata del tipo:

float mion=10.4;
MMP_SetData(mia_MMP,(void*)&mion,sizeof(float),1);

Come vedete gli argomenti da passare sono la struttura MMP precedentemente preparata, il puntatore al dato da incorporare, la lunghezza del dato stesso e per finire l’identificativo del sensore. La funzione utilizza l’allocazione dinamica della memoria, in caso di errori ritorna 0, in caso tutto fili liscio ritorna 1. Mi rendo conto che per chi non ha studiato i puntatori questa funzione paia al limite dell’esoterico, in realtà è molto più semplice di quanto sembri. All’inizio verifica se la lunghezza del dato è uguale o meno a quella precedentemente memorizzata nella struttura, in caso affermativo la memoria allocata è già delle giuste dimensioni e non dobbiamo far altro che copiare in nuovi dati, in caso contrario viene effettuata una riallocazione. La fase successiva copia nel primo byte del buffer l’identificativo del sensore (myMPP->buffer[0]=id;), mentre il ciclo for successivo copia il dato, nel nostro caso un float, byte per byte. Qualcuno potrebbe pensare di eliminare tutto il sistema di allocazione e riallocazione sostituendolo con un array fisso di 256 bytes, non l’ho fatto perchè Arduino UNO ha risorse limitate, per non parlare delle varie ATTiny, per cui lo scopo è risparmiare risorse al massimo. Inoltre spedire ogni volta tutto quel quantitativo di risorse inutili porta ad un intasamento della rete da parte di dati che non servono a nulla.

Di fatto con le istruzioni viste abbiamo già preparato la struttura con i dati da inviare, non ci resta che la trasmissione vera e propria che però dipende dal sistema di comunicazione scelto.

La lettura

La lettura del dato si ottiene con il percorso inverso. Una volta ricevuto il dato complessivo della struttura da analizzare, vengono valutati gruppo ed elemento, se corrispondono entrambi ad uno sappiamo che dobbiamo leggere un dato termico per cui preleviamo il primo byte per l’identificativo del sensore e altri 4 per la misura vera e propria.

float * ptrf=((unsigned char *)(mia_MMP->buffer+1));
float num=*ptrf;
printf(“Valore: %f\n”,num);

Impacchettamento dei dati

Qui cominciano le divergenze in base al supporto fisico di trasmissione. In base a quanto abbiamo detto all’inizio, nell’invio e ricezione della struttura non dobbiamo dimenticarci del byte di inizio e di fine trasmissione che sono fondamentali per ridurre la possibilità di tutta una serie di errori. Ci sono altri sistemi software molto usati per la riduzione degli errori (cercate CRC in rete se siete interessati), ma non è questa la sede adatta per parlarne. Tutto ciò è vero per i supporti fisici che non supportano alcun controllo sui dati inviati, come ad esempio per la RS232 / 485, ma nel caso si utilizzino dei supporti che già di per se effettuano questo tipo di controlli, non sarà necessario includere questi dati accessori.

NRF24

La ricezione ed invio dei dati veri e propri è sostanzialmente indipendente dal mezzo fisico con cui questi sono inviati e ricevuti, prenderò però ad esempio i moduli nrf24 visto che ho ricevuto diverse richieste in tal senso. Ovviamente voi non potete vedere tutte le modifiche che faccio agli articoli che scrivo finché non sono pubblicate, ma vi vorrei dire questo articolo era già stato completato ed ero pronto a pubblicarlo quando ho deciso di ripartire da zero con la scrittura di questo capitolo sull’ NRF24. Avevo usato, come nei precedenti articoli, la libreria Mirf, ma ho avuto tanti problemi. Più nel dettaglio, non essendo nota a priori la lunghezza del pacchetto dati da inviare, ho utilizzato un payload pari a uno, inviavo tutti i singoli bytes e poi la stazione ricevente ricostruiva il messaggio originale per interpretarlo. Dopo giorni e giorni di test ho dovuto arrendermi in quanto vengono persi pacchetti di dati che mandano in tilt tutta la trasmissione. Avrei potuto mettere mano alla libreria per migliorarla, ma avrei dovuto studiare accuratamente come funziona sia la libreria che il modulo e poi iniziare appena a programmare. Un lavoro complesso e dispendioso per un articolo che pensavo di preparare in un paio di giorni :-). Ho dato un’occhiata in rete e a dirla tutta la Mirf non riceve aggiornamenti da diversi anni per cui credo sia dismessa. Ho perciò deciso di utilizzare il lavoro di progetti più recenti ed aggiornati, per cui oggi la mirf sarà abbandonata a favore della RF24Network che andrò a testare per la prima volta. Questa fra le varie, effettua tutta una serie di controlli sui pacchetti dati che inviamo e riceviamo per cui non dobbiamo far altro che limitarci ad inviare la sola struttura MMP e al resto ci penserà la RF24Network.  Tutto questo ovviamente ha un prezzo, infatti l’eseguibile finale è aumentato nel mio progetto  da 15 a 27KB sui 32 disponibili, nonostante l’eliminazione di tutto il sistema di controllo sulla trasmissione, inoltre ho registrato una occupazione di ulteriori 400bytes aggiuntivi (circa) per le variabili globali. Questo in alcuni progetti potrebbe diventare determinante ed impedire l’utilizzo di questa libreria. C’è la possibilità di migliorare questi dati rinunciando ad alcune caratteristiche, cosa che viene fatta di default se compilate su ATTiny con cui la libreria è compatibile. Inoltre vi è la compatibilità anche con RaspBerry, perciò con una singola libreria possiamo scrivere il medesimo software per più piattaforme, cosa da non sottovalutarsi. Ci sono poi caratteristiche interessanti come la possibilità di usare in contemporanea due moduli, uno solo per ricevere e l’altro per trasmettere. Insomma, ha un peso in più ma questa libreria è davvero ben fatta.

Bene, non ci resta che inviare e ricevere i dati. Ovviamente sia l’Arduino che invia che quello che riceve le informazioni deve aver preparato una struttura MMP.

Si potrebbe ad esempio definire globalmente:

struct MMP * mia_MMP;

e poi nel setup iniziale utilizzare:

mia_MMP=MMP_New();

Nulla toglie però la possibilità di usare una variabile locale da allocare solamente quando dobbiamo inviare dei dati o quando sono pronti dati da leggere, in questo modo si riduce il quantitativo di memoria costantemente occupato.

Quando riceveremo i dati in arrivo basterà qualcosa del tipo:

...
  if(mia_MMP->gruppo==1 && mia_MMP->elemento==5)
  {
   float ricevuto=*((float*)((unsigned char *)(mia_MMP->buffer+1)));
......

Nel caso specifico se nella trasmissione ricevuta il gruppo è pari a 1 e l’elemento è pari a 5 andiamo a leggere un float all’interno del buffer inviato con la trasmissione. Si noti il buffer+1 in quanto il primo byte abbiamo detto che lo codifichiamo per identificare l’ipotetico sensore da cui è letta una certa temperatura ma che potrebbe essere l’identificativo di un pulsante, un numero di errore o quel che volete.

    MMP_PrepareGE(mia_MMP,2,1); // Invio temperatura locale a scheda remota
    MMP_SetData(mia_MMP,(float*)&temperatura,sizeof(float),1);
    MMP_Send(mia_MMP); // preparo l'invio alla scheda remota un messaggio di reset.

Dove la funzione PrepareGE non fa altro che settare gruppo ed elemento:

void MMP_PrepareGE(struct MMP* miaMPP,unsigned char G,unsigned char E)
{
   miaMPP->gruppo=G;
   miaMPP->elemento=E; 
}

Oltre ai dati veri e propri dobbiamo settare anche il modulo radio. Ci sono alcune inizializzazioni da fare all’inizio, tanto per cominciare le inclusioni:

#include <RF24Network.h>
#include <RF24.h>
#include <SPI.h>

Passiamo poi a definire alcune variabili globali:

RF24 radio(5,7); // ce /csn; 
RF24Network network(radio);      // Network uses that radio
const uint16_t this_node = 00;    // Address of our node in Octal format ( 04,031, etc)
const uint16_t other_node = 01;   // Address of the other node in Octal format

e per finire facciamo alcune inizializzazioni nel setup:

SPI.begin();
radio.begin();
radio.setPALevel(RF24_PA_MAX);
radio.setDataRate(RF24_250KBPS);
network.begin(/*channel*/ 90, /*node address*/ this_node);

La prima riga (RF 24 radio(5,7) prepara il modulo per utilizzare i pin 5 e 7 rispettivamente come ce e cns, ovviamente vanno cambiati in base a come decidete di collegare il modulo sul vostro Arduino. Con serPALevel settiamo la potenza del trasmettitore e con setDataRate la velocità dello stesso. Come vedete rispetto alla Mirf non andiamo a scrivere direttamente nel registro del modulo ma sfruttiamo delle funzioni pronte all’uso. La RF24Network network(radio);   prepara il network appoggiandosi al modulo appena settato, dopodiché prepariamo due variabili per contenere gli indirizzi ricevente e trasmittente. Si nota che questo è diverso da quello a 5bytes visto con la Mirf e altre librerie, infatti utilizza un sistema di indirizzamento completamente diverso che vi suggerisco di andare a guardare nella documentazione ufficiale. Chiariamolo subito, la RF24Network non è indispensabile per questo tipo di progetto, ma rispetto alla semplice RF24 che trovate nel link proposto più sopra, presenta appunto il vantaggio di smistare i vari messaggi all’interno della rete di moduli nrf24 per cui se un messaggio è diretto ad un destinatario diverso dalla nostra scheda, la libreria si preoccupa di reinviare il messaggio verso il destinatario in modo del tutto trasparente al nostro sketch.

Spendo due righe sugli indirizzi. Come potete notare c’è un nodo con indirizzo 0 che possiamo definirlo come master, il nodo centrale di tutto il sistema. Esso può connettersi ad un massimo di 5 moduli, questo limite è intrinseco ai moduli stessi che permettono un massimo di 6 connessioni contemporanee. Ognuno dei moduli a sua volta permette 6 connessioni, ma una è già impiegata per il collegamento con il master per cui ne restano 5. Comprendendo il master ci possono essere un massimo di 5 livelli, che però scendono a 4 se viene utilizzato il multicast, ossia la possibilità di inviare un messaggio in contemporanea a tutti i moduli di uno stesso livello. Ovviamente l’Arduino collegato al singolo modulo può poi gestirsi tutti i sensori che vuole, potrebbe persino connettersi ad altre reti via rs485, wifi o persino avere un secondo modulo nrf24 connesso a una seconda rete. L’indirizzo è composto da uno 0 iniziale, che in C ricordiamolo indica l’utilizzo della base ottale, seguito da un massimo di 4 numeri, ognuno compreso fra 1 e 5 che indicano i vari “anelli”, ad esempio 01234 sarà il dispositivo 4 del quarto anello, connesso al terzo del terzo, connesso al secondo del secondo, a sua volta connesso al primo del primo e a sua volta connesso al master centrale. L’altra cosa che volevo segnalarvi prima di procedere, è che per ogni singolo messaggio è possibile inviare un identificativo numerico compreso fra 0 e 127 (i successivi sono riservati per gli usi interni della libreria). Quelli compresi fra 65 e 127 ricevono l’ACK (Acknowledge) mentre quelli inferiori a 65 no. Per chi non lo sapesse, l’ack è il messaggio di risposta che il destinatario fornisce al mittente per avvisarlo della corretta ricezione del pacchetto di dati, cosa che incrementa il traffico sulla rete ma rende la trasmissione decisamente più affidabile. Chiusa questa lunga parentesi continuiamo con il nostro progetto. Ora vi mostro la funzione di trasmissione che avevo preparato ma che non funziona:

void MMP_Send(struct MMP* myMMP)
{
  network.update();  
  RF24NetworkHeader header(/*to node*/ other_node);
  bool ok = network.write(header,myMMP,sizeof(struct MMP));
  if(!ok) Serial.println(F("Send failed"));  
  else  Serial.println(F("Send OK"));  
}

Sembra tutto molto semplice, infatti con la funzione write non faccio altro che inviare la struttura che ho in memoria, nulla di più facile. Peccato però che l’ultimo parametro della struttura sia un puntatore ad un buffer interno per cui copiare un indirizzo di memoria in una scheda remota non serve assolutamente a nulla in quanto noi siamo interessati al contenuto di quel buffer e non al suo indirizzo. Devo ammettere che questo problema mi ha fatto perdere quasi due settimane di tempo e probabilmente è causa di gran parte dei problemi che ho avuto con la Mirf che probabilmente ho ingiustamente incolpato dei malfunzionamenti. Ma oramai ho intrapreso questa strada con la RF24-Network e procederemo in questa direzione visti i vantaggi che abbiamo nella costruzione di una rete di sensori. Quel che dobbiamo fare è perciò preparare un’area di memoria che contiene tutti i dati da inviare, compreso il buffer con i dati veri e propri e non il puntatore ad essi:


void MMP_Send(struct MMP* myMMP)
{
  //network.update();  
  RF24NetworkHeader header(/*to node*/ other_node);
  unsigned char * buffer;
  unsigned char ln;
    
  ln=sizeof(struct MMP)-sizeof(unsigned char*)+sizeof(unsigned char)*myMMP->lunghezza;
   
  buffer=(unsigned char*)malloc(ln);
  if(!buffer) return; 

  *buffer=myMMP->sorgente;
  *(buffer+1*sizeof(unsigned char))=myMMP->destinazione;
  *(buffer+2*sizeof(unsigned char))=myMMP->gruppo;
  *(buffer+3*sizeof(unsigned char))=myMMP->elemento;
  *(buffer+4*sizeof(unsigned char))=myMMP->lunghezza;
  memcpy((buffer+5*sizeof(unsigned char)),myMMP->buffer,myMMP->lunghezza);
  bool ok = network.write(header,buffer,ln);
  free(buffer);
}

Di fatto questa funzione prepara un’area di memoria in ci poi copia tutti i dati della nostra struttura e li invia alla scheda remota. Il ricevente deve fare il percorso inverso, copiare i dati ricevuti in un’area di memoria sufficientemente grande e poi riportare i dati nei singoli campi della nostra struttura. La funzione di ricezione potrebbe essere:

unsigned char MMP_Get(struct MMP * myMMP)
{
  network.update();  
  if(network.available())
  {
    RF24NetworkHeader header;        // If so, grab it and print it out
   
    unsigned char * buffer;
    unsigned char ln;
    
    ln=(unsigned char)MAX_PAYLOAD_SIZE;
   
    
    buffer=(unsigned char*)malloc(ln);
    network.read(header,buffer,ln);
    if(!buffer) return(0);

    myMMP->sorgente=*buffer;
    myMMP->destinazione=*(buffer+1*sizeof(unsigned char));
    myMMP->gruppo=*(buffer+2*sizeof(unsigned char));
    myMMP->elemento=*(buffer+3*sizeof(unsigned char));
    myMMP->lunghezza=*(buffer+4*sizeof(unsigned char));
    memcpy(myMMP->buffer,(buffer+5*sizeof(unsigned char)),myMMP->lunghezza);
    free(buffer);
    return(1);
  }

  return(0);

}

La funzione è quasi speculare, l’unica differenza sta nel fatto che non conosciamo a priori la lunghezza del buffer ricevuto e non ho trovato una funzione in grado di farlo anche se sicuramente al suo interno la RF24-Network conosce questo dato e in qualche modo dovrebbe essere prelevabile. L’unico problema è che mancandoci questo dato dobbiamo mantenere la massima dimensione del buffer possibile con uno spreco inutile di risorse che su Arduino sappiamo essere preziose. Risolverò questo problema, ma per ora mi accontento di questi listati funzionanti.

Riassumiamo

Ora mettiamo tutto insieme. Vi accorpo qui sotto tutte le funzioni necessarie:

struct MMP * MMP_New()
{
  struct MMP * myMMP;
  myMMP=(struct MMP*)malloc(sizeof(struct MMP));
  if(!myMMP) return(NULL);
  myMMP->sorgente=0;
  myMMP->destinazione=0;
  myMMP->gruppo=0;
  myMMP->elemento=0;
  myMMP->lunghezza=0;
  myMMP->buffer=NULL;
  return(myMMP);
}

void MMP_Free(struct MMP *myMMP)
{
  if(myMMP->lunghezza) free(myMMP->buffer);
  myMMP->lunghezza=0;
  free(myMMP);
}

unsigned char MMP_SetData(struct MMP *myMMP,void * data,unsigned char ln, unsigned char id)
{
  unsigned char datalen=ln+1;
  unsigned char i;
  Serial.println(myMMP->lunghezza);
  if(myMMP->lunghezza!=datalen)
  {
   myMMP->lunghezza=datalen;
   if(datalen==0) free(myMMP->buffer);
   else
   {
     unsigned char * localbuffer;
     localbuffer=(unsigned char*)realloc(myMMP->buffer,datalen);
     if(!localbuffer) {
       myMMP->lunghezza=0;
       return(0);
     }
     myMMP->buffer=localbuffer;
   }
  }
  myMMP->buffer[0]=id;
  for(i=0;i<ln;i++) *((unsigned char*)myMMP->buffer+1+i)=*((unsigned char*)data+i);
  return(1);
}

void MMP_Send(struct MMP* myMMP)
{
  //network.update();  
  RF24NetworkHeader header(other_node);
  unsigned char * buffer;
  unsigned char ln;
    
  ln=sizeof(struct MMP)-sizeof(unsigned char*)+sizeof(unsigned char)*myMMP->lunghezza;
    
  buffer=(unsigned char*)malloc(ln);
  if(!buffer)  return; 
  *buffer=myMMP->sorgente;
  *(buffer+1*sizeof(unsigned char))=myMMP->destinazione;
  *(buffer+2*sizeof(unsigned char))=myMMP->gruppo;
  *(buffer+3*sizeof(unsigned char))=myMMP->elemento;
  *(buffer+4*sizeof(unsigned char))=myMMP->lunghezza;
  memcpy((buffer+5*sizeof(unsigned char)),myMMP->buffer,myMMP->lunghezza);
  bool ok = network.write(header,buffer,ln);
  free(buffer);
}


unsigned char MMP_Get(struct MMP * myMMP)
{
  network.update();  
  if(network.available())
  {
    RF24NetworkHeader header;       
    unsigned char * buffer;
    unsigned char ln;
    ln=(unsigned char)MAX_PAYLOAD_SIZE;
    buffer=(unsigned char*)malloc(ln);
    network.read(header,buffer,ln);
    if(!buffer) return(0); 
    myMMP->sorgente=*buffer;
    myMMP->destinazione=*(buffer+1*sizeof(unsigned char));
    myMMP->gruppo=*(buffer+2*sizeof(unsigned char));
    myMMP->elemento=*(buffer+3*sizeof(unsigned char));
    myMMP->lunghezza=*(buffer+4*sizeof(unsigned char));
    memcpy(myMMP->buffer,(buffer+5*sizeof(unsigned char)),myMMP->lunghezza);
    free(buffer);
    return(1);
  }
  return(0);
}

void MMP_PrepareGE(struct MMP* miaMPP,unsigned char G,unsigned char E)
{
  miaMPP->gruppo=G;
  miaMPP->elemento=E; 
}

void MMP_PrepareSD(struct MMP* miaMPP,unsigned char S,unsigned char D)
{
  miaMPP->sorgente=S;
  miaMPP->destinazione=D;
}

Non dovete far altro che copiare quanto sopra nel vostro listato senza ulteriori preoccupazioni. Poi magari in futuro converto i listati in C++ e metto il tutto in una comoda libreria esterna.

Esempio riassuntivo

Facciamo alcuni brevi esempi applicativi. Allora, in fondo al listato riportiamo il codice del paragrafo qui sopra e all’inizio quello che vi riporto qui di seguito:

#include <RF24Network.h>
#include <RF24.h>
#include <SPI.h>

struct MMP
{
 unsigned char sorgente;
 unsigned char destinazione;
 unsigned char gruppo;
 unsigned char elemento;
 unsigned char lunghezza;
 unsigned char * buffer;
};

struct MMP * MMP_New(void);
unsigned char MMP_SetData(struct MMP *,void*,unsigned char,unsigned char);
void MMP_Free(struct MMP *);
void MMP_Send(struct MMP*);
unsigned char MMP_Get(struct MMP *);
void MMP_PrepareGE(struct MMP*,unsigned char,unsigned char);
void MMP_PrepareSD(struct MMP* miaMPP,unsigned char,unsigned char);


struct MMP * mia_MMP;
RF24 radio(5,7); // ce /csn; 9 e 10 sulla lcd
RF24Network network(radio);      // Network uses that radio
const uint16_t this_node = 00;    // Address of our node in Octal format ( 04,031, etc)
const uint16_t other_node = 01;   // Address of the other node in Octal format

Attenzione che radio(5,7) cambierà in base a dove avete connesso i pin ce e csn del modulo. Poi non dimentichiamoci delle inizializzazioni da fare nel setup:

mia_MMP=MMP_New();
SPI.begin();
radio.begin();
radio.setPALevel(RF24_PA_MAX);
radio.setDataRate(RF24_250KBPS);
network.begin(/*channel*/ 90, /*node address*/ this_node);

A questo punto siete pronti e dobbiamo passare alla parte pratica. Ricordatevi  che per ogni dato inviato potete selezionare un gruppo ed un elemento che poi siete voi a decidere se utilizzare o meno e nel caso quale significato assegnargli. Inoltre ricordate che nel buffer contenente i dati potete specificare un identificativo univoco (da 0 a 255).

Facciamo finta di voler inviare un valore di temperatura memorizzato in un float. Decidiamo di voler indicare il gruppo 10 quale “lettura sensori” e l’elemento 1 corrispondente alle temperature.

...      
 MMP_PrepareGE(mia_MMP10,1);  // Invio temperatura locale a scheda remota
 MMP_SetData(mia_MMP,(float*)&temperatura,sizeof(float),1); // si noti l'ID=1 che potrei usare per identificare il sensore
 MMP_Send(mia_MMP); // invio i dati
...

e come la leggo?

  if(MMP_Get(mia_MMP)) // controllo se ho ricevuto dati rf24
  {
    Serial.println(“Ricevuti dati…”);
    if(mia_MMP->gruppo==10 && mia_MMP->elemento==1) // ricevuta lettura termica
    {    
      Serial.println(“… temperatura remota: “);
      float temp=(float)(*(float*)(unsigned char *)(mia_MMP->buffer+1));
      Serial.print(float(temp)); //… la stampo su seriale
    }

E ora….

E ora non avete più scuse. Anche se il sistema è sicuramente perfettibile, vi permette di scambiare qualsiasi informazione fra due dispositivi purché i dati veri e propri non superino i 254bytes (1 è riservato all’id). Quindi che siano float, double, interi, piccole stringhe o intere stutture non fa alcuna differenza. State però attenti, visto quanto detto sopra, che eventuali strutture presentino solamente dati statici e non puntatori, in quanto verrebbero inviati solo i puntatori stessi e non il loro contenuto.

Ho testato questo protocollo sul mio sistema di rilevazione delle temperature e dopo un paio di giorni di test continuativo posso dire di non aver notato nessun problema. Purtroppo non ho un terzo modulo per simulare la trasmissione contemporanea di più pacchetti di dati, ma non credo che ci siano motivi per avere problemi.

Mi rendo conto che il progetto è un po’ grezzo, dovete fare tutta una serie di copia e incolla per adattarlo al vostro progetto. Se l’argomento vi interessa posso pensare a scrivere la versione C++ ed includere tutto in una libreria separata, in questo modo si riduce notevolmente il lavoro iniziale di configurazione del progetto