Transmisja danych GPS przez LoRaWAN
Projekt oparty na teście transmisji — zamiast licznika wysyłamy pozycję GPS z Grove Air530. Payload zawiera szerokość, długość, wysokość, liczbę satelitów i HDOP.
Sprzęt
| Urządzenie | Rola |
|---|---|
| XIAO ESP32S3 | mikrokontroler / LoRaWAN node |
| Grove – Wio-E5 (LoRa-E5) | moduł LoRa (AT commands przez UART) |
| Grove – GPS Air530 | odbiornik GPS/GNSS (NMEA przez UART) |
| SenseCAP M2 Multi-Platform Gateway (SX1302, EU868) | bramka LoRaWAN |
| Grove Shield for XIAO | rozszerzenie złączy Grove na XIAO |
Połączenie
ESP32S3 ma trzy sprzętowe UART-y. UART1 zajmuje Wio-E5 przez port Grove UART na shieldzie. GPS Air530 podłączamy do UART2 przez piny breakout na shieldzie.
XIAO ESP32S3 (Grove Shield for XIAO)
│
├─ [UART Grove port] ──► Grove Wio-E5
│ D6 / GPIO43 (TX) → E5 RX
│ D7 / GPIO44 (RX) → E5 TX
│
└─ [pin header / breakout] ──► Grove GPS Air530
D0 / GPIO1 (RX2) ← GPS żółty (TX)
D1 / GPIO2 (TX2) → GPS biały (RX) ← opcjonalnie
3V3 ← GPS czerwony (VCC)
GND ← GPS czarny (GND)
Grove UART na shieldzie jest zajęty przez Wio-E5. GPS łączymy bezpośrednio do pinów breakout na krawędzi shielda — jumperami lub skróconą wtyczką Grove.
Air530 potrzebuje widoku nieba do uzyskania fixa. Zimny start (cold boot) może zająć do 30 s na otwartym terenie. W budynku fix może być niedostępny — kod wysyła pakiet dopiero gdy pozycja jest ważna.
Konfiguracja bramki i TTN
Zarejestruj bramkę SenseCAP M2 (zobacz Bramka) na TTN, a następnie zarejestruj end-node OTAA w tej samej aplikacji TTN. Potrzebujesz: DevEUI, AppEUI, AppKey.
Biblioteki
Zainstaluj przez Arduino IDE → Tools → Manage Libraries:
| Biblioteka | Autor | Zastosowanie |
|---|---|---|
TinyGPS++ | Mikal Hart | parsowanie zdań NMEA z Air530 |
Kod
#include <TinyGPS++.h>
// UART1 → Grove Wio-E5
// TX = GPIO43 (D6), RX = GPIO44 (D7)
#define E5_SERIAL Serial1
#define E5_RX_PIN 44
#define E5_TX_PIN 43
// UART2 → Grove GPS Air530
// RX = GPIO1 (D0) ← GPS TX (żółty)
// TX = GPIO2 (D1) → GPS RX (biały, opcjonalnie)
#define GPS_SERIAL Serial2
#define GPS_RX_PIN 1
#define GPS_TX_PIN 2
TinyGPSPlus gps;
// Klucze TTN — uzupełnij
const char* DEV_EUI = "XXXXXXXXXXXXXXXX";
const char* APP_EUI = "0000000000000000";
const char* APP_KEY = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
// Interwał wysyłania — minimum 60 s dla EU868 (duty cycle 1%)
const uint32_t SEND_INTERVAL_MS = 60000;
// -------------------------------------------------------
bool sendAT(const char* cmd, const char* expected, uint32_t timeout_ms = 4000) {
while (E5_SERIAL.available()) E5_SERIAL.read();
E5_SERIAL.println(cmd);
String resp = "";
uint32_t t0 = millis();
while (millis() - t0 < timeout_ms) {
while (E5_SERIAL.available()) resp += (char)E5_SERIAL.read();
if (resp.indexOf(expected) >= 0) {
Serial.print("[OK] "); Serial.println(resp);
return true;
}
}
Serial.print("[FAIL] "); Serial.print(cmd);
Serial.print(" → "); Serial.println(resp);
return false;
}
bool joinOTAA() {
char buf[80];
sendAT("AT+RESET", "+RESET:", 3000);
delay(2000);
if (!sendAT("AT+MODE=LWOTAA", "+MODE: LWOTAA")) return false;
if (!sendAT("AT+DR=EU868", "+DR: EU868")) return false;
if (!sendAT("AT+CH=NUM,0-7", "+CH: NUM")) return false;
if (!sendAT("AT+CLASS=A", "+CLASS: A")) return false;
snprintf(buf, sizeof(buf), "AT+ID=DevEui,\"%s\"", DEV_EUI);
if (!sendAT(buf, "+ID: DevEui")) return false;
snprintf(buf, sizeof(buf), "AT+ID=AppEui,\"%s\"", APP_EUI);
if (!sendAT(buf, "+ID: AppEui")) return false;
snprintf(buf, sizeof(buf), "AT+KEY=AppKey,\"%s\"", APP_KEY);
if (!sendAT(buf, "+KEY: APPKEY")) return false;
Serial.println("Łączenie z siecią LoRaWAN (OTAA)...");
return sendAT("AT+JOIN", "+JOIN: Network joined", 30000);
}
void sendPayload(const uint8_t* data, uint8_t len) {
char cmd[len * 2 + 20];
strcpy(cmd, "AT+MSGHEX=\"");
for (uint8_t i = 0; i < len; i++) sprintf(cmd + strlen(cmd), "%02X", data[i]);
strcat(cmd, "\"");
sendAT(cmd, "+MSGHEX:", 10000);
}
// -------------------------------------------------------
// Odczyt GPS — czeka max wait_ms na ważne zdanie NMEA
// -------------------------------------------------------
void feedGPS(uint32_t wait_ms) {
uint32_t t0 = millis();
while (millis() - t0 < wait_ms) {
while (GPS_SERIAL.available())
gps.encode(GPS_SERIAL.read());
}
}
// -------------------------------------------------------
void setup() {
Serial.begin(115200);
delay(1000);
E5_SERIAL.begin(9600, SERIAL_8N1, E5_RX_PIN, E5_TX_PIN);
GPS_SERIAL.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);
delay(500);
if (!joinOTAA()) {
Serial.println("JOIN nieudany! Sprawdź: klucze TTN, zasięg bramki, pasmo EU868.");
while (1) delay(5000);
}
Serial.println("Połączono z LoRaWAN!");
Serial.println("Oczekiwanie na fix GPS...");
}
void loop() {
// Czytaj GPS przez 5 s (bez blokowania transmisji LoRa)
feedGPS(5000);
if (!gps.location.isValid()) {
Serial.printf("Brak fixa GPS (satelity: %u, wiek: %lums)\n",
gps.satellites.value(), gps.location.age());
// Nie wysyłaj bez fixa — nie marnuj duty cycle
return;
}
double lat = gps.location.lat();
double lng = gps.location.lng();
float alt = gps.altitude.isValid() ? (float)gps.altitude.meters() : 0.0f;
uint8_t sats = gps.satellites.isValid() ? (uint8_t)gps.satellites.value() : 0;
uint8_t hdop = gps.hdop.isValid()
? (uint8_t)constrain((int)(gps.hdop.hdop() * 10), 0, 255)
: 255;
Serial.printf("GPS: lat=%.6f lng=%.6f alt=%.1fm sats=%u hdop=%.1f\n",
lat, lng, alt, sats, hdop / 10.0f);
// Pakowanie 11 bajtów:
// [0-3] szerokość × 1 000 000 → int32 (np. 52123456 = 52.123456°)
// [4-7] długość × 1 000 000 → int32
// [8-9] wysokość w metrach → int16 (±32767 m)
// [10] liczba satelitów → uint8
// [11] HDOP × 10 → uint8 (np. 12 = 1.2)
int32_t ilat = (int32_t)(lat * 1e6);
int32_t ilng = (int32_t)(lng * 1e6);
int16_t ialt = (int16_t)constrain((int)alt, -32768, 32767);
uint8_t payload[12];
payload[0] = (ilat >> 24) & 0xFF; payload[1] = (ilat >> 16) & 0xFF;
payload[2] = (ilat >> 8) & 0xFF; payload[3] = ilat & 0xFF;
payload[4] = (ilng >> 24) & 0xFF; payload[5] = (ilng >> 16) & 0xFF;
payload[6] = (ilng >> 8) & 0xFF; payload[7] = ilng & 0xFF;
payload[8] = (ialt >> 8) & 0xFF; payload[9] = ialt & 0xFF;
payload[10] = sats;
payload[11] = hdop;
sendPayload(payload, sizeof(payload));
// Pozostały czas interwału (feedGPS już "zjadł" 5 s)
if (SEND_INTERVAL_MS > 5000)
delay(SEND_INTERVAL_MS - 5000);
}
Dekoder payloadu na TTN
- TTN Console → aplikacja → Payload formatters → Uplink.
- Wybierz Custom Javascript formatter, wklej:
function decodeUplink(input) {
var b = input.bytes;
function int32(b, i) {
var v = (b[i] << 24) | (b[i+1] << 16) | (b[i+2] << 8) | b[i+3];
return v | 0; // force signed
}
function int16(b, i) {
var v = (b[i] << 8) | b[i+1];
return (v & 0x8000) ? v - 0x10000 : v;
}
return {
data: {
latitude: int32(b, 0) / 1e6,
longitude: int32(b, 4) / 1e6,
altitude: int16(b, 8),
satellites: b[10],
hdop: b[11] / 10.0
},
warnings: [],
errors: []
};
}
Weryfikacja
Serial Monitor (115200 baud), urządzenie na zewnątrz:
[OK] +MODE: LWOTAA
...
Połączono z LoRaWAN!
Oczekiwanie na fix GPS...
Brak fixa GPS (satelity: 3, wiek: 1230ms)
GPS: lat=52.123456 lng=21.456789 alt=105.3m sats=8 hdop=1.2
[OK] +MSGHEX: Done
W TTN Console → Live data pakiet powinien pokazać zdekodowane współrzędne.
TTN integruje się z Datacake i Cayenne myDevices, gdzie można wyświetlić dane GPS bezpośrednio na mapie bez własnego backendu.
Air530 (cold boot) potrzebuje do 30 s na otwartym niebie. Jeśli po 2–3 minutach nadal brak fixa — sprawdź czy antena GPS ma widok nieba (nie przez beton).