Det er minus fire ute, og du leser det på en liten skjerm inne i stua. Målingen kom med radio fra en værstasjon med ESP32, LoRa og solcelle som står i snøen uten kabler. Her bygger du den selv med to Arduino Nesso N1 og en DHT22, trinn for trinn: først en ping, så ekte målinger med kontroll av tapte pakker, til slutt dvale, solcelle og en ærlig vintertest.
- Nivå
- Nybegynner til avansert
- Tid
- En kveld for nivå 1 og 2, flere uker for nivå 3
- Lodding
- Nei på nivå 1 og 2
- Du lærer
- LoRa, RSSI, sendetid, binære rammer, dyp søvn og energibudsjett
Delene du trenger for værstasjonen
Nivå 1 og 2 krever bare de tre første linjene. Resten kjøper du først når du vet at radio og sensor virker.
Arduino Nesso N12 stk, én sender og én mottaker, med ESP32-C6, SX1262, skjerm og 250 mAh-batteri
DHT22-modul1 stk, måler temperatur og relativ fuktighet
Koblingsledninger hann-hunn3 stk, fra modulens pinner til hullene i utvidelsesporten
DFRobot Solar Power Manager 5V (DFR0559)Nivå 3, lader cellen fra solcellen og gir 5 V
Li-Pol-celle 3500 mAhNivå 3, energilageret for mørke uker
Minisolcelle 1 W / 6 VNivå 3, bare etter at du har målt tomgangsspenningen- Elektronikkapsling i plast, IP65Nivå 3, plast slipper radiosignalet gjennom
MultimeterNivå 3, for å måle strøm og tomgangsspenning
Tre nivåer, og hver av dem virker alene
Du sitter igjen med et fungerende program etter hvert nivå. Får du problemer på nivå 3, kan du alltid gå tilbake og se om radioen fortsatt virker. Det sparer mange timer feilsøking inne i en lukket boks.
| Nivå | Hva er nytt | Senderen viser | Mottakeren viser |
|---|---|---|---|
| 1: ping | Radio og rekkeviddetest | Pingteller og lufttid | RSSI, SNR og alder |
| 2: måling | DHT22, binær ramme, sekvensnummer | Temperatur, fuktighet, batteri | Verdier og tapte pakker |
| 3: vinter | Dyp søvn, energibudsjett, solcelle, kapsling | Kontrollskjerm etter reset, ellers mørk | Varsel om gammel måling |
Nivå 1: første ping og RSSI på skjermen
- Sett antennen i kontakten på begge kortene før du slår dem på. Arduino advarer om at senderen kan ta skade uten antenne.
- Installer kortpakken esp32 by Espressif Systems, minst versjon 3.3.5, og velg Arduino Nesso N1.
- Installer bibliotekene Arduino_Nesso_N1 og RadioLib i bibliotekbehandleren.
- Last opp senderen til det ene kortet og mottakeren til det andre. Merk kortene med en bit teip. Det sparer deg for forvirring senere.
Dette er hele senderen:
// Nivå 1, sender: sender en liten ping-pakke hvert 20. sekund og viser status.
#include <Arduino_Nesso_N1.h>
#include <RadioLib.h>
const float FREKVENS_MHZ = 868.3;
const int8_t EFFEKT_DBM = 14; // 25 mW, se avsnittet om lovlig radio
const uint32_t INTERVALL_MS = 20000;
// Reset ligger på I/O-utvideren og kan ikke gis til RadioLib, derfor RADIOLIB_NC.
SX1262 radio = new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY);
NessoDisplay skjerm;
uint32_t teller = 0;
uint32_t lufttidMs = 0;
uint32_t pauseMs = INTERVALL_MS;
uint32_t sistSendt = 0;
bool foerstePing = true;
void startRadio() {
pinMode(LORA_ENABLE, OUTPUT);
digitalWrite(LORA_ENABLE, LOW); // reset av SX1262
delay(10);
digitalWrite(LORA_ENABLE, HIGH);
delay(10);
pinMode(LORA_LNA_ENABLE, OUTPUT);
digitalWrite(LORA_LNA_ENABLE, HIGH); // mottaksforsterker på
pinMode(LORA_ANTENNA_SWITCH, OUTPUT);
digitalWrite(LORA_ANTENNA_SWITCH, HIGH); // strøm til antennebryteren
// frekvens, båndbredde, SF, kodingsrate, synkord, effekt, preamble, TCXO-spenning, LDO
int status = radio.begin(FREKVENS_MHZ, 125.0, 9, 5, 0x12, EFFEKT_DBM, 8, 3.0, true);
if (status != RADIOLIB_ERR_NONE) {
Serial.printf("Radiofeil %d\n", status);
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setCursor(0, 0);
skjerm.printf("Radiofeil %d", status);
while (true) delay(1000);
}
}
void tegnSkjerm(int status) {
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.print("SENDER");
skjerm.setCursor(0, 24);
skjerm.setTextSize(3);
skjerm.setTextColor(status == RADIOLIB_ERR_NONE ? TFT_GREEN : TFT_RED, TFT_BLACK);
skjerm.printf("Ping %lu", (unsigned long)teller);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 60);
skjerm.printf("%.1f MHz %d dBm", FREKVENS_MHZ, EFFEKT_DBM);
skjerm.setCursor(0, 82);
skjerm.printf("Lufttid %lu ms", (unsigned long)lufttidMs);
}
void setup() {
Serial.begin(115200);
skjerm.begin();
skjerm.setRotation(1);
skjerm.fillScreen(TFT_BLACK);
startRadio();
// Pakken er alltid 10 byte, så lufttiden er den samme hver gang.
lufttidMs = radio.getTimeOnAir(10) / 1000;
pauseMs = max(INTERVALL_MS, lufttidMs * 100); // aldri over 1 % sendetid
Serial.printf("Lufttid %lu ms, pause %lu ms\n",
(unsigned long)lufttidMs, (unsigned long)pauseMs);
}
void loop() {
if (foerstePing || millis() - sistSendt >= pauseMs) {
foerstePing = false;
sistSendt = millis();
teller++;
char pakke[11];
snprintf(pakke, sizeof(pakke), "PING %05lu", (unsigned long)(teller % 100000));
int status = radio.transmit(pakke);
Serial.printf("%s status %d\n", pakke, status);
tegnSkjerm(status);
}
// Nedtelling til neste ping.
uint32_t igjen = (pauseMs - (millis() - sistSendt)) / 1000;
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 104);
skjerm.printf("Neste om %lu s ", (unsigned long)igjen);
delay(250);
}
Og dette er hele mottakeren:
// Nivå 1, mottaker: lytter hele tiden og viser RSSI, SNR og alder på siste pakke.
#include <Arduino_Nesso_N1.h>
#include <RadioLib.h>
const float FREKVENS_MHZ = 868.3;
SX1262 radio = new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY);
NessoDisplay skjerm;
volatile bool nyPakke = false;
bool harPakke = false;
uint32_t sistMottatt = 0;
// Kalles fra avbrudd når radioen har en hel pakke. Gjør så lite som mulig her.
void IRAM_ATTR pakkeMottatt() {
nyPakke = true;
}
void startRadio() {
pinMode(LORA_ENABLE, OUTPUT);
digitalWrite(LORA_ENABLE, LOW);
delay(10);
digitalWrite(LORA_ENABLE, HIGH);
delay(10);
pinMode(LORA_LNA_ENABLE, OUTPUT);
digitalWrite(LORA_LNA_ENABLE, HIGH);
pinMode(LORA_ANTENNA_SWITCH, OUTPUT);
digitalWrite(LORA_ANTENNA_SWITCH, HIGH);
int status = radio.begin(FREKVENS_MHZ, 125.0, 9, 5, 0x12, 14, 8, 3.0, true);
if (status != RADIOLIB_ERR_NONE) {
Serial.printf("Radiofeil %d\n", status);
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setCursor(0, 0);
skjerm.printf("Radiofeil %d", status);
while (true) delay(1000);
}
radio.setPacketReceivedAction(pakkeMottatt);
radio.startReceive();
}
// Fire streker: grensene er tommelfingerregler for denne testen.
void tegnStreker(float rssi) {
int streker = rssi > -80 ? 4 : rssi > -100 ? 3 : rssi > -115 ? 2 : 1;
for (int i = 0; i < 4; i++) {
int h = 10 + i * 10;
uint16_t farge = i < streker ? TFT_GREEN : TFT_DARKGREY;
skjerm.fillRect(176 + i * 15, 66 - h, 11, h, farge);
}
}
void tegnPakke(const String& tekst, float rssi, float snr) {
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.print(tekst);
skjerm.setCursor(0, 26);
skjerm.setTextSize(5);
skjerm.setTextColor(TFT_GREEN, TFT_BLACK);
skjerm.printf("%.0f", rssi);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 76);
skjerm.printf("dBm SNR %.1f dB", snr);
tegnStreker(rssi);
}
void setup() {
Serial.begin(115200);
skjerm.begin();
skjerm.setRotation(1);
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.print("Venter ...");
startRadio();
}
void loop() {
if (nyPakke) {
nyPakke = false;
String tekst;
int status = radio.readData(tekst);
if (status == RADIOLIB_ERR_NONE) {
float rssi = radio.getRSSI();
float snr = radio.getSNR();
Serial.printf("%s RSSI %.0f dBm SNR %.1f dB\n", tekst.c_str(), rssi, snr);
tegnPakke(tekst, rssi, snr);
harPakke = true;
sistMottatt = millis();
} else {
Serial.printf("Lesefeil %d\n", status);
}
radio.startReceive();
}
if (harPakke) {
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 104);
skjerm.printf("Alder %lu s ", (unsigned long)((millis() - sistMottatt) / 1000));
}
delay(100);
}
PING 00011 RSSI -86 dBm SNR 7.5 dB PING 00012 RSSI -87 dBm SNR 7.2 dB
Linjene som ikke er opplagte
new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY): reset sitter på en I/O-utvider over I²C, så RadioLib kan ikke styre den selv. Derfor pulserer viLORA_ENABLEmed vanligdigitalWrite. Klassen erSX1262, samme brikke som Arduinos dokumentasjon oppgir.LORA_LNA_ENABLEogLORA_ANTENNA_SWITCH: uten disse når ikke antennesignalet fram til radioen. Dette er grunnen til at generisk SX1262-kode ofte feiler på akkurat dette kortet.0x12: synkordet for private nett. LoRaWAN bruker0x34, så vårt valg gjør at pakkene ikke later som de er LoRaWAN.max(INTERVALL_MS, lufttidMs * 100): en enkel sikring som holder sendetiden under 1 %, selv om du senere endrer SF.getTimeOnAir(10): RadioLib regner ut lufttiden for 10 byte med de innstillingene du har valgt. Du trenger ikke stole på tallene i denne guiden.
Rekkeviddetest: gå en tur med mottakeren
Begge kortene har batteri, og mottakeren viser alt du trenger. Sett senderen i et vindu, gjerne høyt, med antennen loddrett. Ta så med mottakeren ut.
- Stopp ved hvert målepunkt og vent på minst tre pakker, altså ett minutt.
- Noter RSSI og SNR, og hold antennen minst 20 cm fra kroppen.
- Gå videre helt til alderen passerer 60 sekunder uten ny pakke. Da er du utenfor rekkevidde i akkurat den retningen.
- Gjenta med senderen ute på stedet der stasjonen skal stå. Det er det eneste resultatet som teller.
| Du ser | Betyr omtrent | Gjør dette |
|---|---|---|
| SNR over 0 dB | God margin | Behold oppsettet |
| SNR mellom 0 og -8 dB | Virker, men mindre margin | Sjekk høyde og plassering |
| SNR under -8 dB eller hull i tellingen | Nær grensen for SF9 | Flytt antennen før du endrer noe annet |
Grensene er tommelfingerregler for denne testen, ikke verdier fra et datablad. Vil du ha lengre rekkevidde, har guiden om LoRa-antenne for 868 MHz mer å hente enn noen innstilling i koden.
Hvis det ikke virker: radiofeil på skjermen
Tallet kommer fra RadioLib. -2 betyr at brikken ikke ble funnet, og -707 eller -705 at den ikke svarte riktig på en kommando. Sjekk først at biblioteket Arduino_Nesso_N1 er installert og at riktig kort er valgt. Kildene oppgir ulik spenning for radioens referanseoscillator: M5Stacks eksempel bruker 3,0 V, som vi også bruker, Zephyr-oppsettet for kortet bruker 1,8 V, og Arduinos eksempel bruker bibliotekets standard. Får du feil, prøv å bytte 3.0 til 1.8 i radio.begin på begge kortene.
Hvis det ikke virker: senderen sender, men mottakeren er stille
Frekvens, båndbredde, SF, kodingsrate og synkord må være like på begge sider. Kontroller også at antennen sitter godt, og vent 20 sekunder: senderen er stille mellom hver ping.
Hvis det ikke virker: skjermen blir hvit eller merkelig
Skjermen og radioen deler SPI-bussen. Start alltid skjermen før radioen, slik programmene gjør, og tegn ikke fra avbruddsfunksjonen. Det er derfor pakkeMottatt() bare setter et flagg.
Så mye får du lov til å sende på 868 MHz
Båndet er delt mellom mange brukere, og reglene er enkle når du først har sett dem. I fribruksforskriften § 8, som gjelder i Norge, er grensen for 868,000 til 868,600 MHz 25 mW ERP og 1 % sendetid. 868,3 MHz ligger i dette delbåndet. Sendetiden regnes vanligvis over en time, så 1 % blir 36 sekunder i timen. De europeiske rammene står i ERC Recommendation 70-03.
symboltid = 2^9 / 125 000 Hz = 4,096 ms preamble = 8 + 4,25 = 12,25 symboler nyttelast = 8 + ceil(96 / 36) x 5 = 23 symboler lufttid = 35,25 x 4,096 ms = 144,4 ms pause ved 1 %= 144,4 ms x 100 = 14,4 s nivå 3 = 0,1444 s / 300 s = 0,048 %
Tallet 96 kommer fra 8 ganger 11 byte, minus 4 ganger SF, pluss 28 og pluss 16 for CRC. Delt på 4 ganger SF og rundet opp gir det 3, som ganges med 5 for kodingsraten 4/5.
Test deg selv: er det lov å sende hvert 10. sekund med SF9?
Nei, ikke i dette delbåndet. 0,1444 s delt på 10 s er 1,44 %, altså over grensen på 1 %. Derfor sender nivå 1 hvert 20. sekund.
Nivå 2: ekte målinger og tapte pakker på en DHT22
Modulen har hannpinner, og porten på Nesso har hull. Ledningene som følger med sensoren har hunnkontakt i begge ender, så de passer ikke direkte. Hann-hunn-ledninger gjør jobben. Slå av kortet med to trykk på strømknappen før du kobler.
- Koble pinne 7 (3V3) til + på modulen.
- Koble pinne 3 (G7) til out på modulen. G7 er GPIO7, og i koden heter den
D1. - Koble pinne 1 (GND) til - på modulen.
- Kontroller retningen før du slår på. Under antenneholderen, som du skrur av med sekskantnøkkelen i esken, står navnene på pinnene trykt på kortet: GND, 5V OUT, G7, G2, G6, BAT, 3V3, 5V IN. Arduinos pinout og datablad er uenige om rekkefølgen på pinne 4 og 5, men vi bruker bare pinne 1, 3 og 7. Les også navnene som står trykt ved pinnene på DHT22-modulen, og følg dem hvis rekkefølgen avviker fra tegningen.
Senderen måler én gang i minuttet og pakker alt i en ramme på 11 byte. Tekst som T=-4.2;RH=81.5 ville også virket, men den tar mer lufttid og er verre å kontrollere.
// Nivå 2, sender: måler med DHT22 hvert minutt og sender en binær ramme med sekvensnummer.
#include <Arduino_Nesso_N1.h>
#include <RadioLib.h>
#include <DHT.h>
const float FREKVENS_MHZ = 868.3;
const uint16_t PERIODE_S = 60;
const int16_t SENSORFEIL = -32768;
// Samme ramme på begge sider: 11 byte uten hull mellom feltene.
struct __attribute__((packed)) Ramme {
uint8_t merke; // alltid 0xA7, skiller våre pakker fra andres
uint16_t sekvens; // øker med 1 for hver sending
int16_t temp10; // grader C ganger 10
uint16_t fukt10; // %RH ganger 10
uint16_t batteriMv; // batterispenning i millivolt
uint16_t periodeS; // tid til neste sending
};
SX1262 radio = new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY);
NessoDisplay skjerm;
NessoBattery batteri;
DHT dht(D1, DHT22); // D1 er GPIO7 på utvidelsesporten
uint16_t sekvens = 0;
void startRadio() {
pinMode(LORA_ENABLE, OUTPUT);
digitalWrite(LORA_ENABLE, LOW);
delay(10);
digitalWrite(LORA_ENABLE, HIGH);
delay(10);
pinMode(LORA_LNA_ENABLE, OUTPUT);
digitalWrite(LORA_LNA_ENABLE, HIGH);
pinMode(LORA_ANTENNA_SWITCH, OUTPUT);
digitalWrite(LORA_ANTENNA_SWITCH, HIGH);
int status = radio.begin(FREKVENS_MHZ, 125.0, 9, 5, 0x12, 14, 8, 3.0, true);
if (status != RADIOLIB_ERR_NONE) {
Serial.printf("Radiofeil %d\n", status);
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.printf("Radiofeil %d", status);
while (true) delay(1000);
}
}
void tegnSkjerm(const Ramme& r, int status) {
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.printf("SENDER #%u", r.sekvens);
skjerm.setCursor(0, 24);
skjerm.setTextSize(3);
if (r.temp10 == SENSORFEIL) {
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.print("Sensorfeil");
} else {
skjerm.setTextColor(TFT_GREEN, TFT_BLACK);
skjerm.printf("%.1f C", r.temp10 / 10.0);
skjerm.setCursor(0, 52);
skjerm.printf("%.1f %%", r.fukt10 / 10.0);
}
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 84);
skjerm.printf("Batt %.2f V", r.batteriMv / 1000.0);
skjerm.setCursor(0, 106);
skjerm.printf("Sending %s", status == RADIOLIB_ERR_NONE ? "OK" : "FEIL");
}
void setup() {
Serial.begin(115200);
skjerm.begin();
skjerm.setRotation(1);
skjerm.fillScreen(TFT_BLACK);
batteri.begin();
dht.begin();
startRadio();
delay(2000); // DHT22 trenger litt tid etter at strømmen er på
}
void loop() {
float temp = dht.readTemperature();
float fukt = dht.readHumidity();
bool gyldig = !isnan(temp) && !isnan(fukt);
Ramme r;
r.merke = 0xA7;
r.sekvens = ++sekvens;
r.temp10 = gyldig ? lroundf(temp * 10) : SENSORFEIL;
r.fukt10 = gyldig ? lroundf(fukt * 10) : 0;
r.batteriMv = lroundf(batteri.getVoltage() * 1000);
r.periodeS = PERIODE_S;
int status = radio.transmit((uint8_t*)&r, sizeof(r));
Serial.printf("#%u %.1f C %.1f %%RH status %d\n", r.sekvens, temp, fukt, status);
tegnSkjerm(r, status);
delay(PERIODE_S * 1000UL);
}
Mottakeren leser den samme rammen, og den samme mottakeren bruker du også på nivå 3:
// Nivå 2 og 3, mottaker: pakker ut rammen, teller tapte pakker og varsler når målingen blir gammel.
#include <Arduino_Nesso_N1.h>
#include <RadioLib.h>
const float FREKVENS_MHZ = 868.3;
const int16_t SENSORFEIL = -32768;
struct __attribute__((packed)) Ramme {
uint8_t merke;
uint16_t sekvens;
int16_t temp10;
uint16_t fukt10;
uint16_t batteriMv;
uint16_t periodeS;
};
SX1262 radio = new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY);
NessoDisplay skjerm;
volatile bool nyPakke = false;
bool harRamme = false;
Ramme siste;
uint32_t sistMottatt = 0;
uint32_t mottatt = 0;
uint32_t tapt = 0;
uint32_t omstarter = 0;
float sisteRssi = 0;
float sisteSnr = 0;
void IRAM_ATTR pakkeMottatt() {
nyPakke = true;
}
void startRadio() {
pinMode(LORA_ENABLE, OUTPUT);
digitalWrite(LORA_ENABLE, LOW);
delay(10);
digitalWrite(LORA_ENABLE, HIGH);
delay(10);
pinMode(LORA_LNA_ENABLE, OUTPUT);
digitalWrite(LORA_LNA_ENABLE, HIGH);
pinMode(LORA_ANTENNA_SWITCH, OUTPUT);
digitalWrite(LORA_ANTENNA_SWITCH, HIGH);
int status = radio.begin(FREKVENS_MHZ, 125.0, 9, 5, 0x12, 14, 8, 3.0, true);
if (status != RADIOLIB_ERR_NONE) {
Serial.printf("Radiofeil %d\n", status);
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setCursor(0, 0);
skjerm.printf("Radiofeil %d", status);
while (true) delay(1000);
}
radio.setPacketReceivedAction(pakkeMottatt);
radio.startReceive();
}
// Sekvensnummeret forteller om noe mangler. Regnestykket tåler at telleren går rundt ved 65535.
void teller(uint16_t sekvens) {
if (harRamme) {
uint16_t hopp = sekvens - siste.sekvens;
if (hopp == 0) {
return; // samme pakke to ganger
} else if (hopp < 1000) {
tapt += hopp - 1; // 1 betyr ingen tap
} else {
omstarter++; // telleren startet på nytt: senderen har mistet strøm eller fått reset
}
}
mottatt++;
}
void tegnRamme() {
skjerm.fillScreen(TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.setTextSize(4);
if (siste.temp10 == SENSORFEIL) {
skjerm.setTextColor(TFT_RED, TFT_BLACK);
skjerm.print("Sensor?");
} else {
skjerm.setTextColor(TFT_GREEN, TFT_BLACK);
skjerm.printf("%.1f C", siste.temp10 / 10.0);
skjerm.setTextSize(2);
skjerm.setCursor(168, 12);
skjerm.printf("%.0f %%", siste.fukt10 / 10.0);
}
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 38);
skjerm.printf("RSSI %.0f SNR %.1f", sisteRssi, sisteSnr);
skjerm.setCursor(0, 60);
uint32_t ventet = mottatt + tapt;
skjerm.printf("Tapt %lu/%lu %.1f%%", (unsigned long)tapt, (unsigned long)ventet,
ventet ? 100.0 * tapt / ventet : 0.0);
skjerm.setCursor(0, 82);
skjerm.printf("Batt %.2fV #%u", siste.batteriMv / 1000.0, siste.sekvens);
}
void tegnAlder() {
uint32_t alder = (millis() - sistMottatt) / 1000;
bool gammel = alder > siste.periodeS * 3UL;
skjerm.setTextSize(2);
skjerm.setTextColor(gammel ? TFT_RED : TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 106);
skjerm.printf("Alder %lu s %s ", (unsigned long)alder, gammel ? "GAMMEL" : "");
}
void setup() {
Serial.begin(115200);
skjerm.begin();
skjerm.setRotation(1);
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.print("Venter ...");
startRadio();
}
void loop() {
if (nyPakke) {
nyPakke = false;
Ramme r;
size_t lengde = radio.getPacketLength();
int status = radio.readData((uint8_t*)&r, sizeof(r));
if (status == RADIOLIB_ERR_NONE && lengde == sizeof(r) && r.merke == 0xA7) {
teller(r.sekvens);
siste = r;
harRamme = true;
sistMottatt = millis();
sisteRssi = radio.getRSSI();
sisteSnr = radio.getSNR();
Serial.printf("#%u %.1f C %.1f %%RH %.2f V RSSI %.0f SNR %.1f tapt %lu omstart %lu\n",
r.sekvens, r.temp10 / 10.0, r.fukt10 / 10.0, r.batteriMv / 1000.0,
sisteRssi, sisteSnr, (unsigned long)tapt, (unsigned long)omstarter);
tegnRamme();
}
radio.startReceive();
}
if (harRamme) {
tegnAlder();
}
delay(200);
}
#129 -4.2 C 81.4 %RH 3.92 V RSSI -87 SNR 7.0 tapt 2 omstart 0 #130 -4.2 C 81.5 %RH 3.92 V RSSI -87 SNR 7.2 tapt 2 omstart 0
Slik oppdager mottakeren tapte pakker
Radioen selv forteller ikke at en pakke aldri kom. Det gjør sekvensnummeret. Hvis forrige pakke hadde nummer 127 og denne har 130, mangler 128 og 129. Regnestykket sekvens - siste.sekvens regnes med uint16_t, så det gir riktig svar også når telleren går rundt fra 65535 til 0.
Et stort hopp betyr noe annet: senderen har startet på nytt og begynt å telle fra 1. Da øker vi omstarter i stedet for å melde tusenvis av tapte pakker. På nivå 3 er dette nesten like nyttig som temperaturen, fordi det avslører en sender som har mistet strømmen i kulda.
Test deg selv: hva skjer om du slår av senderen i ti minutter?
Hvis du slår den av, teller den fra 1 igjen, så mottakeren melder én omstart og ingen tapte pakker. Alderen går opp, og når den passerer tre perioder, blir linjen rød. Tar du i stedet mottakeren med utenfor rekkevidde en stund mens senderen står på, er det tapte pakker du ser.
Hvis det ikke virker: «Sensorfeil» på skjermen
Sjekk at out går til G7 og ikke til G2 eller G6. Mål 3,3 V mellom + og - på modulen. Prøv kortere ledninger og vent to sekunder mellom hver lesing: DHT22 er treg og svarer ikke raskere.
Nivå 3: dvale, solcelle og kulde på Nesso N1
Kan Nesso N1 virkelig sove?
Ja. ESP32-C6 har dyp søvn, Arduino anbefaler å bruke den i databladet for kortet, og Espressifs API-er esp_sleep_enable_timer_wakeup() og esp_deep_sleep_start() finnes i kortpakken. Programmet under er kompilert for kortet. Men Espressifs 7 µA gjelder bare selve brikken.
På kretsskjemaet ser du at mye annet fortsatt har strøm: spenningsregulatoren, ladekretsen AW32001, batterimåleren BQ27220, to I/O-utvidere, en liten styrekrets for strømknappen, skjermkontrolleren og bevegelsessensoren. Arduino oppgir ikke noe tall for hele kortet i dvale, og vi har ikke målt det. Derfor er det du selv som må måle.
// Nivå 3, sender: våkner, måler, sender, slår av alt som kan slås av og går i dyp søvn.
// Strømknappen (ett trykk) gir reset og viser en kontrollskjerm i 10 sekunder.
#include <Arduino_Nesso_N1.h>
#include <RadioLib.h>
#include <DHT.h>
#include "esp_sleep.h"
const float FREKVENS_MHZ = 868.3;
const uint16_t PERIODE_S = 300;
const int16_t SENSORFEIL = -32768;
const float LADING_MIN_C = 2.0; // ingen lading av Li-ion nær eller under 0 grader
const float LADING_MAX_C = 40.0;
struct __attribute__((packed)) Ramme {
uint8_t merke;
uint16_t sekvens;
int16_t temp10;
uint16_t fukt10;
uint16_t batteriMv;
uint16_t periodeS;
};
// RTC-minnet overlever dyp søvn, men ikke reset eller strømbrudd.
RTC_DATA_ATTR uint16_t sekvens = 0;
SX1262 radio = new Module(LORA_CS, LORA_IRQ, RADIOLIB_NC, LORA_BUSY);
NessoBattery batteri;
DHT dht(D1, DHT22);
int startRadio() {
pinMode(LORA_ENABLE, OUTPUT);
digitalWrite(LORA_ENABLE, LOW);
delay(10);
digitalWrite(LORA_ENABLE, HIGH);
delay(10);
pinMode(LORA_LNA_ENABLE, OUTPUT);
digitalWrite(LORA_LNA_ENABLE, HIGH);
pinMode(LORA_ANTENNA_SWITCH, OUTPUT);
digitalWrite(LORA_ANTENNA_SWITCH, HIGH);
return radio.begin(FREKVENS_MHZ, 125.0, 9, 5, 0x12, 14, 8, 3.0, true);
}
void stoppRadio() {
radio.sleep(); // SX1262 i dvale
digitalWrite(LORA_LNA_ENABLE, LOW); // mottaksforsterkeren av
digitalWrite(LORA_ANTENNA_SWITCH, LOW); // antennebryteren av
}
void visKontrollskjerm(const Ramme& r, int status, bool lader, float batteriC) {
NessoDisplay skjerm;
skjerm.begin(); // slår også på bakgrunnslyset
skjerm.setRotation(1);
skjerm.fillScreen(TFT_BLACK);
skjerm.setTextSize(2);
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 0);
skjerm.printf("DVALE-NODE #%u", r.sekvens);
skjerm.setTextColor(TFT_GREEN, TFT_BLACK);
skjerm.setCursor(0, 24);
if (r.temp10 == SENSORFEIL) {
skjerm.print("Sensorfeil");
} else {
skjerm.printf("%.1f C %.0f %%", r.temp10 / 10.0, r.fukt10 / 10.0);
}
skjerm.setTextColor(TFT_WHITE, TFT_BLACK);
skjerm.setCursor(0, 46);
skjerm.printf("Batt %.2f V %.0f C", r.batteriMv / 1000.0, batteriC);
skjerm.setCursor(0, 68);
skjerm.printf("Lading %s", lader ? "PA" : "AV");
skjerm.setCursor(0, 90);
skjerm.printf("Sending %s", status == RADIOLIB_ERR_NONE ? "OK" : "FEIL");
skjerm.setTextColor(TFT_YELLOW, TFT_BLACK);
skjerm.setCursor(0, 112);
skjerm.printf("Dvale om 10 s");
delay(10000);
skjerm.sleep(); // skjermen i dvale
digitalWrite(LCD_BACKLIGHT, LOW); // bakgrunnslyset av
pinMode(GROVE_POWER_EN, OUTPUT);
digitalWrite(GROVE_POWER_EN, LOW); // 5 V-omformeren til Grove og utvidelsesporten av
}
void setup() {
bool kaldstart = esp_reset_reason() != ESP_RST_DEEPSLEEP;
if (kaldstart) {
Serial.begin(115200);
}
dht.begin();
int status = startRadio();
if (kaldstart) {
batteri.begin(); // laderens grunninnstillinger, trengs bare etter reset
}
float batteriC = batteri.getTemperature();
bool lader = batteriC >= LADING_MIN_C && batteriC <= LADING_MAX_C;
batteri.setChargeEnable(lader);
float temp = dht.readTemperature();
float fukt = dht.readHumidity();
if (isnan(temp) || isnan(fukt)) { // ett nytt forsøk etter sensorens minstepause
delay(2100);
temp = dht.readTemperature();
fukt = dht.readHumidity();
}
bool gyldig = !isnan(temp) && !isnan(fukt);
Ramme r;
r.merke = 0xA7;
r.sekvens = ++sekvens;
r.temp10 = gyldig ? lroundf(temp * 10) : SENSORFEIL;
r.fukt10 = gyldig ? lroundf(fukt * 10) : 0;
r.batteriMv = lroundf(batteri.getVoltage() * 1000);
r.periodeS = PERIODE_S;
if (status == RADIOLIB_ERR_NONE) {
status = radio.transmit((uint8_t*)&r, sizeof(r));
}
stoppRadio();
if (kaldstart) {
Serial.printf("#%u %.1f C %.1f %%RH radio %d, batteri %.0f C, lading %d\n",
r.sekvens, temp, fukt, status, batteriC, lader);
Serial.flush();
visKontrollskjerm(r, status, lader, batteriC);
}
esp_sleep_enable_timer_wakeup(PERIODE_S * 1000000ULL);
esp_deep_sleep_start();
}
void loop() {
// Nås aldri: etter dvale starter programmet på nytt fra setup().
}
RTC_DATA_ATTR: variabelen ligger i minnet som holder seg gjennom dyp søvn, slik at sekvensnummeret ikke starter på nytt hver gang.stoppRadio(): radioen legges i dvale, og mottaksforsterkeren og antennebryteren slås av før brikken sover. I/O-utviderne har egen strøm og beholder innstillingene sine mens ESP32-C6 sover.digitalWrite(LCD_BACKLIGHT, LOW): bakgrunnslyset lyser ellers videre mens brikken sover. Det er den klassiske grunnen til at batteriet er tomt etter en dag.setChargeEnable(lader): slår av ladingen av kortets interne celle når batterimåleren viser under 2 °C.
Hvis det ikke virker: USB-porten forsvinner, og du får ikke lastet opp
Brikken kobler fra USB i dyp søvn, så porten forsvinner fra datamaskinen. Hold strømknappen inne for å gå i bootloader-modus, slik Arduino beskriver det, og last opp på nytt.
Slik måler du strømmen i dvale
Cellen i kortet er bygd inn, så du kan ikke sette et multimeter i serie uten å åpne det. Her er to metoder som virker uten det.
- Tømmetesten, uten verktøy. Lad kortet fullt, trekk ut USB og la det stå i romtemperatur. Mottakeren viser når pakkene stopper. Del 250 mAh på antall timer, og du har et grovt gjennomsnitt. Hvis det tar fem døgn, altså 120 timer, er snittet rundt 2 mA.
- Multimeteret, med ekstern celle. Bygg kjeden i neste avsnitt og vent til den interne cellen er full. Sett multimeteret i serie med plussledningen fra cellen til BAT IN, med laveste strømområde.
- Hold måleren ute av toppene. Legg en ledning som kortslutter måleren mens noden er våken. Måleren har en motstand i serie, og på de laveste områdene kan spenningsfallet bli så stort at kortet starter på nytt. Fjern ledningen når noden sover, og les av.
- Tell den aktive tiden. Mål tiden fra forbruket stiger til det faller. Sett begge tallene inn i kalkulatoren under.
Multimetermetoden måler hele kjeden, også omformeren i DFR0559. DFRobot oppgir ikke hvilestrømmen, så dette tallet kan bare du finne. Mer om hvilket instrument som passer, står i guiden om multimeter, oscilloskop og USB-tester.
Startverdiene i kalkulatoren er antakelser, ikke målinger: 60 mA i ett sekund og 0,5 mA i hvile. De gir rundt 0,7 mA og cirka 125 døgn på cellen på 3500 mAh. Med kortets egen celle på 250 mAh blir det rundt 9 døgn. Brukbar andel er satt til 60 % fordi celler gir mindre i kulde og ikke bør tømmes helt.
Solcelle og DFR0559: tomgangsspenningen avgjør
DFRobot oppgir disse grensene for DFR0559: solinngang 4,4 til 6 V, og tomgangsspenningen må aldri være over 6,5 V, ellers kan modulen bli varig skadet. Modulen lader én 3,7 V-celle med opptil 900 mA og gir 5 V opptil 1 A. Velg en celle som tåler ladestrømmen; ved 3500 mAh er 900 mA godt under 0,5 C. Modulen avslutter ladingen ved 4,2 V, har overspenningsvern over 4,3 V og kutter utladingen ved 2,4 V, men sjekk også om cellen selv har vern. Kontakten på cellen kan være en annen enn PH2.0 på modulen, så sjekk polariteten og bruk skrueklemmen på BAT IN.
Her ligger en felle få guider nevner: tomgangsspenningen øker når det blir kaldt. For silisiumceller er endringen rundt 2,2 mV per grad per celle. Et panel med 12 celler i serie stiger dermed med omtrent 1,2 V mellom 25 og -20 °C. En kald og klar vinterdag er den farligste dagen for inngangen.
- Mål tomgangsspenningen med multimeter i full sol, med panelet frakoblet. Mål helst når panelet er kaldt, og regn ut fra temperaturen på selve panelet, ikke lufttemperaturen. Et panel i sol er varmere enn lufta.
- Legg til kuldetillegget. Ved +20 °C bør et panel med 12 celler altså vise under omtrent 5,3 V for å holde seg under 6,5 V ved -20 °C.
- Er spenningen høyere, skal panelet ikke kobles direkte til DFR0559. En seriediode hjelper ikke, for uten strøm faller det nesten ingen spenning over den.
Merkingen 6 V sier ikke hva tomgangsspenningen er, og vi har ikke målt panelet på 1 W. Panelet har loddepunkter og må beskyttes mot vær. Er det for høyt, finnes en annen vei: solcelleladeren for 6 til 24 V med et værbestandig 5 W-panel. Les grensene og anbefalt celle for den modulen før du kobler. Hvor mye energi du kan regne med på stedet ditt i det verste døgnet, finner du i PVGIS, og guiden om solcelle på hytta viser hvorfor desember skal dimensjonere systemet, ikke juli.
Kondens og kapsling: den tette boksen er problemet
En boks med høy IP-klasse holder regnet ute, men den holder også fuktig luft inne. Når temperaturen faller, kondenserer fukten på det kaldeste stedet, gjerne lokket, og drypper ned på kretskortet. Trykkforskjellen ved hver temperaturendring kan dessuten trekke ny fuktig luft inn gjennom en dårlig pakning.
- Trykkutjevning: en membranventil lar luft og damp passere, men ikke vann.
- Tørkepose: tar opp fukten som er låst inne. Bytt den når du åpner boksen.
- Kabler nedover: kabelnipler i bunnen og dryppsløyfe, aldri gjennomføring i lokket.
- Sensoren utenfor: DHT22 inne i boksen måler boksen, ikke været. Sett den i en hvit strålingsskjerm i skygge, med fri luftgjennomstrømning og tak mot regn. Husk 1 m-grensen for ledningen ved 3,3 V.
Vintertesten i kjøleskap og fryser
Dette er ikke en sertifisering, men en billig måte å finne de dumme feilene på før snøen gjør det. Lad aldri under testen, og ha alltid USB frakoblet.
- Kjøleskapet, rundt 4 °C. Hele noden i 24 timer, innenfor oppgitt område. Noter hvor mye RSSI faller når døren lukkes: kjøleskapet og fryseren er metallbokser, så tapte pakker her skyldes ofte ikke kulden.
- Bare sensoren i fryseren. Legg DHT22 i fryseren med noden utenfor, ledningen under 1 m og gjennom pakningen for en kort stund. Sammenlign med et termometer.
- Hele noden i fryseren, på egen risiko. Dette er utenfor spesifikasjonen. Bruk en lukket pose med tørkemiddel, cellen halvfull, og ikke mer enn et par timer. Se om den våkner, og om spenningen synker raskt.
- La den varmes opp i posen. Vent noen timer før du åpner. Åpner du en kald node i varm stue, setter fukten seg på kretskortet med en gang.
Om noden starter og sender i kulde, om spenningen holder under sending, om sensoren følger referansen og om omstarter dukker opp.
Rekkevidde ute, sol og vind, snø på panelet, UV på plast, måneder med temperatursykler, aldring av cellen og oppførsel ved lading i kulde. Det siste skal du ikke teste.
Bygge selv eller velge en ferdig værstasjon?
Denne prototypen lærer deg hvordan alt henger sammen, men den er ikke bygd for å stå ute i årevis. SenseCAP S2120 8-i-1 er et ferdig alternativ med solforsyning, vind, nedbør og et oppgitt måleområde for lufttemperatur fra -40 til 80 °C. Sjekk produktsiden for hva som gjelder for batteriet i kulda. Den snakker LoRaWAN, så den trenger dekning eller en egen gateway.
| Velg | Passer når | Ulempe |
|---|---|---|
| Prosjektet i denne guiden | Du vil lære, og stasjonen står frostfritt eller er en test | Du må måle og vedlikeholde alt selv |
| Ferdig LoRaWAN-stasjon | Målingene skal stå ute og være til å stole på | Trenger LoRaWAN-dekning |
| Sett med vind og nedbør | Du vil måle vind og regn med egen elektronikk | Du må selv lese de passive sensorene |
| Enkel stasjon med display | Du vil bare lese temperaturen inne og ute | Ingen vind og ingen logg |
Værstasjon med vindmåler og app: hva bør du velge?
Prosjektet i denne guiden måler bare temperatur og fuktighet, og det har ingen app. Vil du ha vind, nedbør og app, er S2120 den ferdige veien, og den kan også leses i SenseCAP-appen.
Vil du bygge selv, kan du koble settet med vind og nedbør til en mikrokontroller. Da blir mye av denne guiden fortsatt nyttig, men du må selv skrive koden for sensorene.
Værstasjon med WiFi eller LoRa?
Velg WiFi når stasjonen står innenfor rekkevidde av ruteren, for eksempel i hagen. Velg LoRa når den står på hytta, i et uthus eller et annet sted WiFi ikke når, og mottakeren står inne hos deg.
Kortene finner du under ESP32 og LoRa og LoRaWAN, og paneler under solceller og solpaneler.
Vanlige spørsmål
Kan Nesso N1 stå ute ved -20 °C?
Det er utenfor produktets oppgitte 0 til 40 °C. En test som virker er ikke dokumentasjon på levetid, og cellen skal uansett ikke lades i den temperaturen.
Hvor langt rekker LoRa med dette oppsettet?
Det avhenger av høyde, terreng og bygninger, så vi lover ikke noe tall. Rekkeviddetesten på nivå 1 gir svaret for din tomt, og flytting av antennen hjelper vanligvis mer enn endrede innstillinger.
Kan jeg sende oftere enn hvert femte minutt?
Radioreglene tillater det: med SF9 er minste pause 14,4 s ved 1 %. Batteriet er det som setter grensen, så regn på hva det koster med kalkulatoren.
Hvorfor ikke WiFi, når kortet har det?
WiFi krever at senderen er innenfor rekkevidde av ruteren og kobler seg til hver gang den våkner. LoRa kan sende direkte over lengre avstand med et kort signal, og det er hele poenget med prosjektet.
Kan et panel merket 6 V kobles til DFR0559?
Bare hvis den målte tomgangsspenningen, med kuldetillegg, holder seg under 6,5 V. Merkingen alene sier ikke nok.
Hva er en værstasjon?
Det er et målepunkt som registrerer været der det står, ofte temperatur og fuktighet, og noen ganger også vind, nedbør og trykk. Målingene vises på en skjerm eller sendes videre, slik denne stasjonen gjør over LoRa.
Kan jeg bruke et annet ESP32-kort med LoRa?
Ja, men koden må endres. Nesso N1 har radioen innebygd, og pinnene i programmene gjelder bare dette kortet. Et annet kort trenger en LoRa-modul på 868 MHz og riktige pinner i koden.
Snakker værstasjonen med Meshtastic eller LoRaWAN?
Nei. Den sender egne pakker på 868,3 MHz. Meshtastic i Norge ligger på 869,525 MHz med standardprofilen eller 869,442 MHz med NarrowFast, og LoRaWAN-gatewayer forstår ikke pakkeformatet. Se Meshtastic-guiden for Norge hvis du heller vil bruke det nettet.
Kilder og veien videre
Pinner og strøm kommer fra Arduinos fulle pinout, databladet og brukerhåndboken for Nesso N1, med radiooppsettet kontrollert mot M5Stacks LoRa-eksempel. Radioreglene står i fribruksforskriften og ERC Recommendation 70-03. Dvale står i Espressifs dokumentasjon og databladet for ESP32-C6, grensene for laderen hos DFRobot, og tallene for temperatur og tomgangsspenning hos PVEducation. Solenergi på ditt sted finner du i PVGIS. Programmene er kompilert for kortet, men vi har ikke kjørt dem på maskinvare, så kontrollpunktene er din test.
Vil du videre? Les om LoRa-antenner for bedre rekkevidde, eller gå til LoRaWAN og Meshtastic når to noder ikke lenger er nok. Nytt for deg med ESP32? Start med ESP32 for nybegynnere.




