Introduzione
A volte, dalle discussioni e scambio di idee che avvengono nel forum, nascono esperimenti, spunti, idee di progetto. Le migliori di queste, vengono trasposte nel blog, in modo da essere più fruibili e più facilmente rintracciabili. Quanto segue, è frutto dell’utente Ippogrifo, che ha sperimentato una soluzione per aiutare gli utenti nei forum, e a cui vanno i miei complimenti.
Zzed
Cos’è LoRa?
Con l’acronimo LoRa si intende una tecnologia di telecomunicazione RF a lungo raggio (Long Range), meglio conosciuta come LoRaWAN (Low Power Wide Area Network). Questa tecnologia è applicabile a sistemi con interazioni lente, esempio variazioni di temperature interne/esterne, umidità, ecc., quindi con fenomeni lenti nel tempo; non può essere applicata a sistemi che richiedono “interventi” in tempo reale.
L’architettura del sistema si basa su tre elementi:
- gli End Node, distribuiti sul campo per raccogliere i dati da sensori vari
- i gateway che comunicano con gli end node ed istradano i dati verso i server
- i vari server che gestiscono i dati ricevuti dai gateway.
LoRaWAN è una rete con struttura a stella (e/o di stelle) senza capacità di comunicazione fra gli end node (ovvero gli end node non possono comunicare tra loro). Il gateway è il centro stella: raccoglie i dati trasmessi dagli end node e li inoltra, usando una connessione con tradizionali protocolli IP, al Network Server (NS) per una prima gestione dei dati stessi. Questi sono successivamente inviati all’ Application Server (AS) che ha il compito di estrarre il dato utile e renderlo disponibile all’applicazione finale. Per maggiori informazioni, in rete è possibile trovare documentazione molto più dettagliata sull’intero processo.
Questo a livello home è forse eccessivo, non necessario; una connessione punto-multipunto potrebbe soddisfare le esigenze del momento (ad esempio per l’IoT).
Una possibilità è la realizzazione di una struttura con un Master e n. Slaves utilizzando dei moduli end-node Tx/Rx LoRa; il master ogni x secondi (minuti, ecc) farà il polling degli slaves ed attenderà la risposta da ciascuno di essi per elaborarla successivamente.
Per sperimentare questa tecnologia di comunicazione a lungo raggio (sino a 10 Km in visibilità ottica) sono stati utilizzati diverse tipologie di moduli end_node.
END-NODE RYLR998
I piccoli moduli LoRa RYLR998 sono interfacciabili tramite connessione seriale UART e comandi AT. Per la realizzazione degli slaves e master sono stati utilizzati dei Raspberry Pi Pico. Successivamente il master pi pico è stato sostituito da un Raspberry Pi4b e SO bookworm. Non sono state testate altre versioni/modelli di Pi Pico.
Il protocollo radio LoRa degli end-node è parte del firmware stesso dei moduli , così come i parametri di configurazione che ne deteminano l’operatività. Questo, alla fine, definisce le funzioni che dovrà svolgere un Pi Pico utilizzato come slave/master e/o di un Pi4 come master: interfaccia per la configurazione, codifica/decodifica dei comandi/dati da inviare e risposte/dati da ricevere.
Prendendo spunto da diversi siti in rete che trattavano l’argomento in micropyton, è stato adottato lo stesso linguaggio per la programmazione dei Raspberry Pi Pico e, quindi, Python per i Raspberry Pi4b (con SO bookworm; in quest’ultimo nel caso non già presente, andrà installata la libreria PiSerial per la gestionedella porta seriale). Thonny come ambiente.
I moduli RYLR998 consentono una elevata possibilità di cofigurazioni, hanno una potenza RF in uscita di 22 dBm (150 mW circa), equipaggiati con antenna elicoidale “on board”. Tensione di alimentazione = 3,3 VDC; tensione linee Tx/Rx uart = 3,3VDC, compatibili con Pi Pico e Rpi4b.
I moduli sono di default così configurati:
- velocità uart: 115200 bit/sec 8 N 1 (velocità di connessione tra modulo e Pi Pico o Pi4b)
- banda:915MHz (configurata successivamente per 868 MHz)
- Spreading Factor:9
- Bandwidth:125kHz
- Coding Rate:1
- Preamble Length:12
- Address:0 (successivamente configurati diversamente per master e slaves)
- Network ID:18
- CRFOP:22 dBm
La configurazione può essere modificata tramite i comandi AT.
Il tempo che intercorre tra una interrogazione del master (polling) alla risposta dello slave è funzione della configurazione. Con i parametri di default utilizzati (e le variazioni minimali apportate) il tempo di risposta è di citca 2-3 secondi per un pacchetto dati di 10-20 byte.
Il modulo, di default, è in modalità Tx/Rx.
Essendo il sistema aperiodico, ovvero non vi è alcuna sincronizzazione nel polling, molto importanti si sono dimostrate le temporizzazioni, in particolare modo tra l’invio dei comandi dal master (funzione send_command()) e l’attesa della risposta da parte degli slaves.
Sugli slaves è stato attivato il secondo core, come thread. Su uno degli slaves il codice nel thread “comunica” in i2c con un modulino INA219 preposto alla lettura della tensione di alimentazione del modulo stesso (3,3 V). Sugli altri slaves viene prodotto un numero randomico (funzione random.random()). I dati (lettura tensione, numeri randomici) vengono presentati alla funzione main tramite variabile globale da cui verranno inviati poi al master. In questo caso il master si limita a mostrare i dati ricevuti sul display.
Sul Pi4b dovrà essere abilitata la porta seriale: da terminale digitare “sudo raspi-config”, selezionare “Interface Option” , successivamente “Serial Port” (Enable/Disable….); scegliere NO per “Would …login shell …”; scegliere SI per attivare la porta seriale. Uscire dalla configurazione.
Per verificare la capacità di collegamento, sono stati efferruati, velocemete, alcuni test nel centro abitato (non quindi in visibilità ottica): la “coperura” è stata di alcune decine di metri.
Affinchè master e slaves possano comunicare, è necessario che i parametri di confgurazione su tutti i dispositivi siano identici.
La sperimentazione, realizzata con un master (Pi4b), tre slaves (Pi Pico) ed i codici in micropython/python per Slaves e Master, è fine a se stessa, ovvero non vi sono applicazioni che li integrano per svolgere un qualsiasi lavoro.
Collegamenti tra Pi Pico slaves e moduli LoRa:

|
Pi Pico |
RYLR998 |
|
Tx – GPIO 0 —> |
Pin RXD |
|
Rx – GPIO 1 <— |
Pin TXD |
|
Gnd – GPIO38 —> |
Pin GND |
|
+3,3V-GPIO36 —> |
Pin VDD |
Collegamenti tra Pi4b master e modulo LoRa:
|
Pi4b |
RYLR998 |
|
Tx-GPIO14 —> |
Pin RXD |
|
Rx-GPIO15 <— |
Pin TXD |
|
GND-Pin6 —> |
Pin GND |
I +3,3V per alimentare il modulo LoRa sono stati derivati da un pccolo step-down +5V –> +3,3V, collegando il Pin2 (+5V) e il Pin6 (GND) rispettivamente all’ingesso “VIN” e “GND” dello step-down. L’uscita (a 3,3V) “OUT” e “GND” sono state collegate rispettivamente ai Pin VDD e GND del modulo LoRa.

Dai links di seguito è possibile scaricare il manuale tecnico e la guida ai comandi AT:
- https://reyax.com//upload/products_download/download_file/LoRa_AT_Command_RYLR998_RYLR498_EN.pdf
- https://reyax.com//upload/products_download/download_file/RYLR998_EN.pdf
Versione SW/FW utilizzato:
- Firmware su Pi Pico: RPI-PICO-20250911-v1.26.1.uf2
- Python 3.11.2 per Raspberry Pi4b
Gli Slaves RYLR998:

Codice Pi Pico Slave:
(con modulo INA219)
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 |
import utime import random import _thread from machine import I2C, Pin from ina219 import INA219 # Inizializzazione UART # Pico GPIO0 = UART0 TX -> LoRa RXD # Pico GPIO1 = UART0 RX -> LoRa TXD uart = machine.UART(0, baudrate=115200, tx=machine.Pin(0), rx=machine.Pin(1)) # LED di bordo (indica che il Pi Pico è operativo) led = machine.Pin("LED", machine.Pin.OUT) # LED che lampeggia indicando l'esecuzione del thread e l'attività del Pi Pico led_on = machine.Pin(13, machine.Pin.OUT) # LED che indica la qualità del segnale (RSSI) ricevuto dal LoRa led_rssi = machine.Pin(18, machine.Pin.OUT) LoRa_Master = 1 # Indirizzo del Master di sistema Lora_Slave = 2 # Indirizzo locale del dispositivo Slave response_ok = False # Flag che indica che esiste una risposta pronta da inviare response = "" # Risposta contenente i dati da inviare al Master running = True # Usato dal secondo core per mantenere attivo il loop # Funzione che invia un comando AT al modulo LoRa def send_command(command): if isinstance(command, str): # Se il comando è una stringa command = command.encode('ascii') # Convertilo in bytes uart.write(command + b"\r\n") # I comandi AT terminano con CR+LF utime.sleep(0.5) # Pausa per permettere al modulo di rispondere data = b"" # Buffer per la risposta UART while uart.any(): data += uart.read() # Legge tutti i byte disponibili if data: print("Risposta:", data.decode('utf-8', 'ignore')) # Inizializza il modulo LoRa configurandolo come Slave def inizialize_slave(): global Lora_Slave print("Inizializzazione Slave") send_command("AT") # Verifica comunicazione con il modulo send_command(f"AT+ADDRESS={Lora_Slave}") # Imposta l'indirizzo dello Slave send_command("AT+NETWORKID=18") # Imposta il network ID send_command("AT+BAND=868000000") # Banda europea 868 MHz #send_command("AT+PARAMETERS=9,7,1,12") # Parametri di default del modulo LoRa print("Slave inizializzato. In attesa di messaggi...") # Verifica se messaggi in arrivo via LoRa def listen_for_messages(): global response_ok global LoRa_Master rx_buffer = b"" # Buffer temporaneo per la ricezione UART if uart.any(): # Se ci sono dati in arrivo data = uart.read() # Legge i dati dal UART if data: rx_buffer += data # Aggiunge al buffer # I messaggi LoRa terminano con CR+LF if b"\r\n" in rx_buffer: lines = rx_buffer.split(b"\r\n") # Rimuove i caratteri di terminazione \r\n da lines rx_buffer = lines.pop() # Rimuove l’ultimo elemento vuoto (b"") for line in lines: data_str = line.decode('utf-8', 'ignore').strip() # Controlla se il messaggio è nel formato +RCV if data_str.startswith("+RCV="): # Formato atteso: +RCV=<addr>,<len>,<msg>,<rssi>,<snr> parts = data_str.split(",") if len(parts) >= 3: # Estrae l’indirizzo del Master mittente if '=' in parts[0]: LoRa_Master = parts[0].split('=')[1] length = parts[1] message = parts[2] if len(parts) > 3: rssi = parts[3] snr = parts[4] print(f"Messaggio ricevuto dal Master n.{LoRa_Master}: {message}") # Segnala al ciclo principale che deve inviare una risposta response_ok = True # Accende un LED se il segnale è abbastanza forte if int(rssi) <= 110: led_rssi.value(1) else: # Altri dati non appartenenti ai pacchetti +RCV if data_str: print("Dato ricevuto:", data_str) if led_rssi(): led_rssi.value(0) # Secondo core: legge il sensore INA219 e prepara la risposta da inviare al Master def secondCore(): global response global running # Inizializzazione I2C (GP4=SDA, GP5=SCL) i2c = I2C(0, sda=Pin(4), scl=Pin(5)) devices = i2c.scan() if devices: print("Dispositivi I2C trovati:", [hex(device) for device in devices]) else: print("Nessun dispositivo I2C trovato! Controllare cablaggi.") # Inizializzazione sensore INA219 ina = INA219(i2c) ina.set_calibration_32V_1A() # Loop continuo sul secondo core while running: current = ina.current voltage = ina.bus_voltage # Filtra rumore a bassissimo livello if current <= 0.05: current = 0 if voltage <= 0.05: voltage = 0 print("{:.2f} mA {:.2f} V".format(current, voltage)) # Il valore della tensione sarà inviato al Master response = str(voltage) + " Volt" # Lampeggiamento LED per indicare operatività del secondo core e Pi Pico led_on.value(1) utime.sleep(1) led_on.value(0) utime.sleep(1) # Invio della risposta al Master def send_response(): global LoRa_Master global response print("Invio risposta al Master n.", LoRa_Master) length = len(response) # Lunghezza del messaggio command = f"AT+SEND={LoRa_Master}, {length+1}, {response}" send_command(command) # Avvio del secondo core per lettura sensore _thread.start_new_thread(secondCore, ()) # Ciclo principale del programma def main(): try: global response_ok global response inizialize_slave() led.value(1) # LED acceso: Pi Pico attivo while True: listen_for_messages() utime.sleep(0.1) if response_ok: send_response() response_ok = False led_rssi.value(0) except KeyboardInterrupt: # Arresto pulito del programma led.value(0) running = False utime.sleep(0.5) # Avvio del programma principale main() |
Il Pi4b RYLR998 Master:

Codice Pi4b Master:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 |
import serial from time import sleep import RPi.GPIO as GPIO # Inizializzazione UART # Raspberry Pi → Modulo LoRa via UART # GPIO14 = TX → LoRa RXD # GPIO15 = RX → LoRa TXD uart = serial.Serial("/dev/ttyS0", 115200, timeout=6) GPIO.setmode(GPIO.BCM) LED_ON = 18 GPIO.setup(LED_ON, GPIO.OUT) GPIO.setwarnings(False) LoRa_Master = 1 LoRa_Slave = 2 # Indirizzo iniziale degli Slave della rete response_ok = False n_attemps = 0 # Tentativi di invio prima di cambiare Slave # Controlla se ci sono dati disponibili nella UART def data_any(): n_byte = uart.in_waiting # Numero di byte nel buffer UART # Se i byte sono troppo pochi, probabilmente non è un messaggio valido if n_byte <= 5: uart.reset_input_buffer() return 0 else: return n_byte # Funzione che invia un comando AT allo Slave def send_command(command): if isinstance(command, str): command = command.encode('ascii') # Converte la stringa in bytes sleep(0.1) # print(command + b"\r\n") # DEBUG uart.write(command + b"\r\n") # Invia il comando AT sleep(0.6) # Attesa per la risposta listen_for_response() # Attende la risposta dallo Slave # Inizializzazione del Master LoRa def inizialize_master(): global LoRa_Master print("Inizializzazione Master") send_command("AT") # Verifica la comunicazione # send_command("AT+PARAMETER=9,7,1,12") # Parametri di default del modulo LoRa send_command(f"AT+ADDRESS={LoRa_Master}") # Imposta indirizzo Master send_command("AT+NETWORKID=18") # Imposta network ID send_command("AT+BAND=868000000") # Banda europea 868 MHz print("Master inizializzato") # Invio del messaggio allo Slave def send_message(): global LoRa_Slave message = "send data" # Messaggio da inviare length = len(message) command = f"AT+SEND={LoRa_Slave}, {length+1}, {message}" # print("command=", command) # DEBUG send_command(command) # Ascolta la risposta dallo Slave dopo l'invio def listen_for_response(): global response_ok rx_data = b"" # Buffer dei dati ricevuti while data_any(): # Finché ci sono dati in arrivo rx_data = uart.read_until() # Legge fino a \r\n # print("Risposta =", rx_data) # DEBUG if b"\r\n" in rx_data: # Messaggio completo ricevuto lines = rx_data.split(b"\r\n") # Divide per fine riga, rimuove terminazione \r\n rx_data = lines.pop() # Elimina l'ultimo elemento vuoto b"" for line in lines: rx_data_str = line.decode('utf-8', 'ignore').strip() # Controlla se il formato è quello LoRa if rx_data_str.startswith("+RCV="): # Formato: +RCV=<addr>,<len>,<msg>,<rssi>,<snr> rx_parts = rx_data_str.split(",") if len(rx_parts) >= 3: slave = rx_parts[0].split('=')[1] length = rx_parts[1] message = rx_parts[2] rssi = rx_parts[3] if len(rx_parts) > 3 else None snr = rx_parts[4] if len(rx_parts) > 4 else None print(f"Risposta ricevuta dallo Slave n.{slave}: {message}") # print(f"RSSI: {rssi}, SNR: {snr}") uart.reset_input_buffer() response_ok = True else: # Altri tipi di linee ricevute (debug, errori, ecc.) if rx_data_str: print("Linea ricevuta:", rx_data_str) # Se non riceve risposta dopo N tentativi, passa allo Slave successivo def verify_attemps(): global LoRa_Slave global n_attemps n_attemps = 0 # Reset dei tentativi LoRa_Slave += 1 # Passa allo Slave successivo if LoRa_Slave > 5: # Se supera il massimo, riparte da 2 LoRa_Slave = 2 print("Cambiato Slave, nuovo indirizzo:", LoRa_Slave) print("") # Funzione principale def main(): global LoRa_Slave global response_ok global n_attemps inizialize_master() # Configura il Master LoRa while True: GPIO.output(LED_ON, GPIO.HIGH) # LED ON → ciclo trasmissione - ricezione in corso # Se non abbiamo ancora ricevuto risposta dallo Slave if response_ok == False: print("Invio messaggio allo Slave n.", LoRa_Slave) send_message() sleep(0.5) listen_for_response() sleep(0.7) n_attemps += 1 # Troppi tentativi senza risposta → cambiamo Slave if n_attemps > 4: verify_attemps() sleep(1.1) # Abbiamo ricevuto risposta correttamente elif response_ok == True: response_ok = False verify_attemps() GPIO.output(LED_ON, GPIO.LOW) # LED OFF → ciclo completato sleep(0.5) main() |
Se vuoi restare aggiornato, seguici anche sui nostri social: Facebook, X, Youtube
Trova prodotti di elettronica, making e accessori Raspberry Pi in offerta, supportaci e risparmia seguendo il nostro canale delle offerte su Telegram!!
RaspberryItaly Community Italiana