Tag Dashboard OLED
XIAO nRF54LM20A · GPS + modem su schermo
Dashboard su OLED Expansion: legge il GPS DAN-F10N (fix / satelliti / coordinate) e interroga il modem EG800Q-EU (operatore / segnale / registrazione), più la batteria via nPM1300. Usa tutte e 3 le UART del nRF54LM20A insieme — GPS su Serial1, modem su Serial ri-pinnata su D8/D9, OLED su Wire — con l'OLED come console.
Seeed XIAO nRF54LM20A Arduino · core nrf54l15clean
Componenti usati
nRF54LM20A
MCU · Cortex-M33 128 MHz
Nordic Semiconductor
DAN-F10N
GNSS dual-band L1/L5
u-blox
EG800Q-EU
Modem LTE Cat-1 bis
Quectel
nPM1300
PMIC · batteria
Nordic Semiconductor
SSD1306
OLED 128×64 (Expansion)
—
tag_dashboard.ino
264 righe GitHub
/*
* VISLA TAG — dashboard su OLED Expansion (SSD1306 128x64).
* Legge GPS (DAN-F10N) e interroga il modem (EG800Q-EU) e mostra a schermo
* fix/satelliti/coordinate + operatore/segnale/registrazione.
*
* nRF54LM20A ha 3 periferiche seriali raggiungibili sull'header (tutte P1):
* UARTE21 = Serial1 -> GPS (D6/D7, connettore Grove) @38400
* TWIM22 = Wire -> OLED (D4/D5) I2C 0x3C
* UARTE20 = Serial -> MODEM (ri-pinnata D8/D9) @115200 [niente console USB]
* L'OLED e' la nostra "console": lo stato lo vediamo lì.
*/
#include <U8g2lib.h>
#include <Wire.h>
#include "npm1300.h" // batteria via nPM1300 (bus TWIM23, separato dall'OLED)
// OLED full-buffer su HW I2C (Wire = TWIM22, D4/D5)
U8G2_SSD1306_128X64_NONAME_F_HW_I2C oled(U8G2_R0, U8X8_PIN_NONE);
#define GPS Serial1 // UARTE21 D6/D7 (Grove)
#define MODEM Serial // UARTE20 ri-pinnata su D8/D9
// ---- stato GPS ----
double gLat = 0, gLon = 0;
int gFix = 0, gSats = 0;
bool gValid = false;
int gsvTot[6] = {0}; // sat in vista per costellazione
uint32_t gGpsBytes = 0, gLastFixMs = 0;
char line[120];
int ln = 0;
// ---- stato modem ----
int mCsq = 99, mReg = -1, mAct = -1;
char mOper[24] = "---";
uint32_t mBytes = 0;
bool mSeen = false;
uint32_t bootMs = 0;
// % batteria dall'nPM1300 (LiPo: 3.30V=0%, 4.20V=100%). -1 se non leggibile
static int battPct() {
int32_t mv = npm1300_read_vbat_mv();
if (mv <= 0) return -1;
int pct = (mv - 3300) * 100 / (4200 - 3300);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
return pct;
}
static int talkerIdx(char a, char b) {
if (a == 'G' && b == 'P') return 0;
if (a == 'G' && b == 'L') return 1;
if (a == 'G' && b == 'A') return 2;
if ((a == 'G' && b == 'B') || (a == 'B' && b == 'D')) return 3;
if (a == 'G' && b == 'Q') return 4;
return -1;
}
// estrae il campo idx (0-based) da una frase NMEA in out[]
static void field(const char *s, int idx, char *out, int outsz) {
int f = 0, o = 0;
out[0] = 0;
for (int i = 0; s[i]; i++) {
char c = s[i];
if (c == ',' || c == '*') {
if (f == idx) { out[o] = 0; return; }
f++; o = 0;
if (c == '*') break;
} else if (f == idx && o < outsz - 1) {
out[o++] = c;
}
}
if (f == idx) out[o] = 0;
}
// ddmm.mmmm + emisfero -> gradi decimali
static double nmeaDeg(const char *v, const char *h) {
if (strlen(v) < 3) return 0;
double val = atof(v);
int deg = (int)(val / 100);
double m = val - deg * 100;
double d = deg + m / 60.0;
if (h[0] == 'S' || h[0] == 'W') d = -d;
return d;
}
static void parseNMEA(const char *s) {
if (s[0] != '$' || strlen(s) < 7) return;
const char *t = s + 3;
char f[16];
if (!strncmp(t, "GGA", 3)) {
field(s, 6, f, sizeof f); gFix = atoi(f);
field(s, 7, f, sizeof f); gSats = atoi(f);
gValid = gFix > 0;
if (gValid) {
char la[16], ns[4], lo[16], ew[4];
field(s, 2, la, sizeof la); field(s, 3, ns, sizeof ns);
field(s, 4, lo, sizeof lo); field(s, 5, ew, sizeof ew);
gLat = nmeaDeg(la, ns);
gLon = nmeaDeg(lo, ew);
gLastFixMs = millis();
}
} else if (!strncmp(t, "GSV", 3)) {
field(s, 3, f, sizeof f);
int idx = talkerIdx(s[1], s[2]);
if (idx >= 0) gsvTot[idx] = atoi(f);
}
}
static void feedGPS() {
while (GPS.available()) {
char c = GPS.read();
gGpsBytes++;
if (c == '\n') { line[ln] = 0; parseNMEA(line); ln = 0; }
else if (c != '\r' && ln < (int)sizeof(line) - 1) { line[ln++] = c; }
}
}
// invia un comando AT, raccoglie la risposta, TENENDO VIVO il GPS nel frattempo
static void modemCmd(const char *cmd, char *resp, int rsz, uint32_t to) {
while (MODEM.available()) MODEM.read();
MODEM.print(cmd);
MODEM.print("\r\n");
int o = 0;
resp[0] = 0;
uint32_t st = millis();
while (millis() - st < to) {
feedGPS();
while (MODEM.available()) {
char c = MODEM.read();
mBytes++;
if (o < rsz - 1) resp[o++] = c;
}
resp[o] = 0;
if (strstr(resp, "OK") || strstr(resp, "ERROR")) break;
delay(2);
}
resp[o] = 0;
if (o > 0) mSeen = true;
}
static void pollModem() {
char r[180];
modemCmd("AT+CSQ", r, sizeof r, 400);
char *p = strstr(r, "+CSQ:");
if (p) mCsq = atoi(p + 5);
modemCmd("AT+COPS?", r, sizeof r, 800);
p = strstr(r, "+COPS:");
if (p) {
char *q = strchr(p, '"');
if (q) {
q++; int i = 0;
while (*q && *q != '"' && i < 22) mOper[i++] = *q++;
mOper[i] = 0;
char *c2 = strchr(q, ',');
if (c2) mAct = atoi(c2 + 1);
}
}
modemCmd("AT+CEREG?", r, sizeof r, 400);
p = strstr(r, "+CEREG:");
if (p) { char *c = strchr(p, ','); if (c) mReg = atoi(c + 1); }
}
// formatta un grado decimale SENZA %f (molti core non linkano il float printf)
static void fmtCoord(double v, char *out) {
char sign = (v < 0) ? '-' : ' ';
if (v < 0) v = -v;
int deg = (int)v;
long frac = (long)((v - deg) * 100000.0 + 0.5);
if (frac >= 100000) { deg++; frac -= 100000; }
sprintf(out, "%c%d.%05ld", sign, deg, frac);
}
static void draw() {
oled.clearBuffer();
oled.setFont(u8g2_font_6x10_tf);
char b[28];
oled.drawStr(0, 8, "VISLA TAG");
int bp = battPct(); // % batteria in alto a destra
if (bp >= 0) {
snprintf(b, sizeof b, "%d%%", bp);
oled.drawStr(128 - oled.getStrWidth(b), 8, b);
}
oled.drawHLine(0, 11, 128);
int inview = 0;
for (int i = 0; i < 6; i++) inview += gsvTot[i];
if (gValid) snprintf(b, sizeof b, "GPS FIX %d sat", gSats);
else snprintf(b, sizeof b, "GPS .. %d in vista", inview);
oled.drawStr(0, 22, b);
if (gValid) {
char la[16], lo[16];
fmtCoord(gLat, la);
fmtCoord(gLon, lo);
snprintf(b, sizeof b, "%s%s", la, lo);
oled.drawStr(0, 33, b);
} else {
oled.drawStr(0, 33, "cerco fix...");
}
snprintf(b, sizeof b, "NET %s", mSeen ? mOper : "(modem?)");
oled.drawStr(0, 44, b);
const char *rt = mReg == 1 ? "casa" : mReg == 5 ? "roaming" :
mReg == 2 ? "cerco" : mReg == 3 ? "negato" : "--";
const char *at = mAct == 7 ? "4G" : mAct == 9 ? "NB" : mAct == 0 ? "2G" :
mAct == 3 ? "E" : mAct == 2 ? "3G" : "?";
if (mCsq == 99 || mCsq < 0) snprintf(b, sizeof b, "%s no segnale", at);
else snprintf(b, sizeof b, "%s %d dBm", at, -113 + 2 * mCsq);
oled.drawStr(0, 55, b);
snprintf(b, sizeof b, "rete: %s", rt);
oled.drawStr(0, 64, b);
snprintf(b, sizeof b, "%lus", (unsigned long)((millis() - bootMs) / 1000));
oled.drawStr(128 - oled.getStrWidth(b), 64, b); // uptime in basso a destra
oled.sendBuffer();
}
void setup() {
bootMs = millis();
npm1300_begin(); // ADC batteria nPM1300
oled.begin();
oled.setFont(u8g2_font_6x10_tf);
oled.clearBuffer();
oled.drawStr(0, 20, "VISLA TAG");
oled.drawStr(0, 34, "avvio GPS+modem...");
oled.sendBuffer();
pinMode(LED_GREEN, OUTPUT); digitalWrite(LED_GREEN, HIGH); // spento (active-low)
pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_BLUE, HIGH);
GPS.begin(38400); // Serial1 UARTE21 (D6/D7)
MODEM.setPins(PIN_D9, PIN_D8); // rx=D9, tx=D8 (P1) su UARTE20
MODEM.begin(115200);
delay(300);
char r[120];
modemCmd("AT", r, sizeof r, 500);
modemCmd("ATE0", r, sizeof r, 500); // echo off -> risposte pulite
modemCmd("AT+CMEE=1", r, sizeof r, 500);
}
void loop() {
feedGPS();
uint32_t now = millis();
static uint32_t lastPoll = 0, lastDraw = 0, lastBeat = 0;
if (now - lastPoll > 3000) { pollModem(); lastPoll = now; }
if (now - lastDraw > 250) { draw(); lastDraw = now; }
// heartbeat: LED verde ON quando c'e' fix GPS, altrimenti lampeggia blu
if (now - lastBeat > 500) {
lastBeat = now;
if (gValid) { digitalWrite(LED_GREEN, LOW); digitalWrite(LED_BLUE, HIGH); }
else { digitalWrite(LED_GREEN, HIGH); digitalWrite(LED_BLUE, !digitalRead(LED_BLUE)); }
}
}