Projekt: LoRa Beacon für Meshtastic
Kleiner Meshtastic Beacon, der einmal pro Zeiteinheit einen Wert in das Meshtastic Netz postet.
Worum geht es hier?
Es geht darum das Prinzip zu Verstehen, wie man in ein bestehendes LoRa Meshtastic Netz einen Beacon installiert, der z.B. Messwerte zyklisch sendet.
Was brauchen wir?
Zuerst das, was nicht passiert: Ich erkläre hier nicht, wie man mit Arduino IDE oder VSC Code generiert oder wie man lötet. Auch bitte ich zu bedenken, dass der erste „Proof ob Concept“ keine Dauerlösung sein darf – In „erster Näherung“ sendet der Beacon nämlich an öffentliche Kanäle und so würde jeder Node in der Umgebung zugespamt werden. Bitte richtet euch auf alle Fälle einen privaten Kanal ein für sowas – Wie das geht, erkläre ich am Ende nochmal genau.
Wir brauchen einen uC für die Messwertaufnahme und das Housekeeping. Da Geräte, die im Freien irgendwo zugänglich „herumhängen“ auch gerne mal Flügel bekommen, also geklaut oder stumpf zerstört werden, soll es nicht all zu teuer zugehen. Stellt euch vor ihr nutzt einen derzeit heiß begehrten Raspberry Pi 2 W oder gar einen Pi 5 – Da freut der „Finder“ gleich doppelt. Darum benutze ich folgendes:
- ESP32 C3 Super Mini mit kleinem OLED Display. Ca. 6€
- HT-RA62 LoRa Modul – Auch ca. 6€
- Ein PETG Gehäuse mit Fach für Powerbank plus ein entsprechendes USB Kabel, Wasserdicht gestaltet und Antennenbuchse nach außen geführt.
- Eine billige Powerbank unter 10€ mit >5000mAh.
Wir landen also um die 20€ und korrekt: Da nur die Temperatur zu übermitteln ging einfacher. Es können so beliebige Werte an mehrere Empfänger „gepostet“ werden und dafür ist der Preis okay.
Hardware aufsetzen

Ich nutze hier bequemerweise einfach die ARDUINO IDE und habe das Board ESP32 C3 „SuperMini“ korrekt enabled. „Hello World“ läuft auf dem OLED und die LEDs kann ich ansprechen. Das bedeutet: Der ESP32 hat verloren und ist unter Kontrolle. (Immer MEIN TIP: Geht Schrittweise vor!)
Dann benötigt ihr die RadioLib, die wir mit „radiolib.h“ einbinden.
Die Hardware: Verbaut zwischen 3,3V Speistung und GND einen PUFFER-ELKO. Sonst kann es sein, dass euch beim Senden die Spannungsversorgung versagt. Je nach Platz sind 10..100uF Okay.
Der ESP32 kommt bei mir tatsächlich ohne WLAN Möglichkeit. Das ganze Anpassungsnetzwerk fehlt. So oder so: Soll der ESP das LoRa-Modul versorgen, lasst WLAN vorsorglich aus. LoRa und WLAN ist mitunter etwas hart für den kleinen LDO.
Der Code
Der Code als SKELETTON:
#include <RadioLib.h>
#include <U8g2lib.h>
// --- KONSTANTEN ---
const int PIN_LORA_NSS = 10;
const int PIN_LORA_DIO1 = 9;
const int PIN_LORA_RST = 8;
const int PIN_LORA_BUSY = 7;
const int PIN_I2C_SDA = 4;
const int PIN_I2C_SCL = 5;
const float LORA_FREQ = 868.0;
const float LORA_BW = 250.0;
const int LORA_SF = 11;
const int LORA_CR = 4;
const int LORA_SYNC_WORD = 0x2B;
const int LORA_TX_POWER = 17;
// OLED Setup (z.B. SH1106 oder SSD1306)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C u8g2(U8G2_R0, /* reset=*/ U8X8_PIN_NONE, PIN_I2C_SCL, PIN_I2C_SDA);
SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_RST, PIN_LORA_BUSY);
void updateDisplay(String status) {
u8g2.firstPage();
do {
u8g2.setFont(u8g2_font_ncenB08_tr);
u8g2.drawStr(0, 20, "Status:");
u8g2.drawStr(0, 40, status.c_str());
} while (u8g2.nextPage());
}
void setup() {
u8g2.begin();
updateDisplay("Initialisierung...");
SPI.begin(6, 2, 3, PIN_LORA_NSS);
if (radio.begin(LORA_FREQ, LORA_BW, LORA_SF, LORA_CR, LORA_SYNC_WORD) == RADIOLIB_ERR_NONE) {
radio.setOutputPower(LORA_TX_POWER);
updateDisplay("Bereit.");
} else {
updateDisplay("LoRa Fehler!");
}
}
void loop() {
String message = "Messwert: 22.5";
updateDisplay("Sende...");
// CAD Prüfung (LBT)
if (radio.startCAD() == RADIOLIB_ERR_NONE && radio.scanCAD() != RADIOLIB_PREAMBLE_DETECTED) {
radio.transmit(message);
updateDisplay("Gesendet!");
} else {
updateDisplay("Kanal belegt");
}
delay(60000);
}
STEP 2: Persönlicher Kanal
Wie oben geschrieben spamt ihr natürlich jeden Node zu, der sich in der Nähe befindet und einen öffentlichen Kanal hat.
Das ist nicht die feind Art. Aber kompliziert ist der Umgang mit dem notwendigen protobuf wiederum leider auch… Darum:
#include <RadioLib.h>
#include <U8g2lib.h>
// --- KONSTANTEN ---
const uint8_t CHANNEL_HASH[4] = {0xAA, 0xBB, 0xCC, 0xDD}; // HIER DEINEN HASH EINTRAGEN!
const int PIN_LORA_NSS = 10;
const int PIN_LORA_DIO1 = 9;
const int PIN_LORA_RST = 8;
const int PIN_LORA_BUSY = 7;
SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_RST, PIN_LORA_BUSY);
void sendPrivatePacket(String message) {
uint8_t buffer[64];
// 1. Header: Channel Hash (Privater Kanal)
memcpy(buffer, CHANNEL_HASH, 4);
// 2. Meshtastic Protobuf Tags
buffer[4] = 0x08; buffer[5] = 0x01; // hop_limit = 1
buffer[6] = 0x52; buffer[7] = 0x01; // portnum = TEXT_MESSAGE
buffer[8] = 0x5A; buffer[9] = (uint8_t)message.length();
// 3. Payload
for(int i = 0; i < message.length(); i++) {
buffer[10 + i] = message[i];
}
// 4. CAD & Senden
if (radio.startCAD() == RADIOLIB_ERR_NONE && radio.scanCAD() != RADIOLIB_PREAMBLE_DETECTED) {
radio.transmit(buffer, 10 + message.length());
}
}
und Verschlüsselung käme so ins Spiel: Ja… dann beachtet bitte, dass das AES vom HELTEC üblicherweise BASE64 ist und ihr das korrekt auf 32 Bit übertragen müsst.
#include "mbedtls/aes.h"
uint8_t aesKey[32] = { /* Deine Key-Bytes hier */ };
void sendEncryptedPacket(String message) {
uint8_t buffer[128];
uint8_t encryptedPayload[message.length()];
// 1. AES-256-CTR Verschlüsselung
// Benötigt: IV/Nonce (bestehend aus Node-ID + Counter)
uint8_t iv[16] = {0}; // Hier muss dein Zähler/Nonce rein!
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
mbedtls_aes_setkey_enc(&aes, aesKey, 256);
size_t nc_off = 0;
uint8_t stream_block[16] = {0};
mbedtls_aes_crypt_ctr(&aes, message.length(), &nc_off, iv, stream_block,
(uint8_t*)message.c_str(), encryptedPayload);
mbedtls_aes_free(&aes);
// 2. Paket zusammenbauen
memcpy(buffer, CHANNEL_HASH, 4);
buffer[4] = 0x08; buffer[5] = 0x01; // hop_limit = 1
buffer[6] = 0x1A; // Tag für "encrypted" statt "decoded"
// ... restliche Protobuf-Tags für das verschlüsselte Feld ...
// Kopiere das verschlüsselte Payload statt dem Klartext
memcpy(buffer + 10, encryptedPayload, message.length());
radio.transmit(buffer, 10 + message.length());
}
