Cada planta de cliente lleva un gateway LoRaWAN indoor con 4G propio, y todos los Smiley del sitio le hablan por radio. El gateway reenvía por 4G a un ChirpStack central de LaEmpresa, que publica los eventos por MQTT al backend de WorkDone.
WiFi y 4G por Smiley quedan como opcionales para actualizar la app; los votos y fichajes NFC viajan siempre por LoRaWAN, con cola local offline-first.
La radio solo cubre el tramo Smiley → gateway dentro de la planta (50–500 m); desde el gateway todo es IP por 4G.
El mismo ChirpStack atiende a todas las plantas: cada gateway se registra con su EUI y cada Smiley con su DevEUI y claves OTAA. Sumar un cliente es sumar un gateway y dar de alta sus nodos.
Recomendado para producción: RAK7268CV2 como gateway y Wio-E5 mini o RAK3172 como módulo en cada Smiley. Todos operan en AU915 y hablan con ChirpStack. Con la tablet Samsung, el módulo es el puente BLE.
| Equipo | Rol | Specs clave | Precio orientativo |
|---|---|---|---|
| RAK7268CV2 WisGate Edge Lite 2 (LTE) | Recomendado | SX1302, 8 canales, LTE Cat4 (EC25-AU para LATAM), PoE o 12 V, SD para buffer si cae el 4G | ~AUD 463 en Australia; cotizar distribuidor LATAM |
| Milesight UG65 (4G) | Alternativa robusta | SX1302, 8 canales, 4G opcional, carcasa IP65, >2000 nodos, ~2 km urbano | Cotizar |
| SenseCAP M2 AU915 | Local, sin 4G integrado | SX1302, WiFi, PoE; el 4G iría por router externo | ARS 310.800 (sin stock) |
| HAT LoRaWAN SX1303 + Raspberry Pi | Piloto / DIY | Concentrador SX1303 915 MHz sobre Pi; 4G por HAT o módem USB | ARS 246.750 + Pi |
| Equipo | Interfaz con el panel | Specs clave | Precio orientativo |
|---|---|---|---|
| Kit XIAO nRF52840 + Wio-SX1262 | BLE (puente, firmware propio) | SX1262 862–930 MHz, BLE 5.0; para la tablet Samsung | ARS 44.900 |
| Wio-E5 mini | USB-C directo (USB-UART integrado) | STM32WLE5JC, firmware AT LoRaWAN Clase A/B/C, AU915 | ~US$ 20–25 |
| RAK3172 | UART (o breakout + USB-TTL) | STM32WLE5, LoRaWAN 1.0.3, AU915, AT; para placa propia | ~US$ 10–15 |
| USB a LoRa SX1262 industrial | USB | Probablemente solo P2P: confirmar LoRaWAN antes de comprar | ARS 44.625 |
Precios en US$/AUD aproximados, de tiendas del exterior, sin envío ni impuestos de importación.
ChirpStack v4 corre en Docker (network server + application server + PostgreSQL + Redis + broker MQTT). El backend WorkDone se suscribe al MQTT y nunca habla con los gateways.
application/+/device/+/event/up desde Spring Boot (Spring Integration MQTT o Paho).Entra en el peor data rate de AU915 (11 bytes de carga útil), así ningún mensaje se pierde por tamaño.
| Tipo (byte 0) | Evento | Campos | Bytes |
|---|---|---|---|
0x01 | Voto | puntaje (1 B) · timestamp epoch (4 B) · secuencia (2 B) | 8 |
0x02 | Fichaje NFC | UID tarjeta (4–7 B) · timestamp comprimido (3 B) | 8–11 |
0x03 | Latido | votos en cola (2 B) · versión app (1 B) · flags (1 B) | 5 |
0x04 | Lote de votos | hasta 3 votos compactos (3 B c/u) | 10 |
Argentina usa el plan AU915 (915–928 MHz, regulado por ENACOM); gateways y módulos se compran en esa variante (TTN Argentina).
Con 10 Smiley por planta, LoRaWAN cuesta en hardware parecido a poner 4G en cada equipo, pero paga 1 abono mensual en vez de 10. Montos en US$ aproximados, antes de importación; a validar con cotización.
| Concepto | LoRaWAN + gateway 4G | 4G en cada Smiley |
|---|---|---|
| Gateway RAK7268CV2 + antena + PoE | 1 × ~US$ 350 | — |
| Radio en cada Smiley | 10 × ~US$ 25 = US$ 250 | 10 × ~US$ 40–60 = US$ 400–600 |
| Hardware por planta | ~US$ 600 | ~US$ 400–600 |
| SIM IoT (S = abono mensual de 1 línea) | 1 × S | 10 × S |
| Dependencia de señal 4G | Un solo punto, elegido y con antena | Cada baño; muchos sin señal |
| ChirpStack central | Compartido entre clientes | No aplica |
Punto de equilibrio: desde unos 4–5 Smiley por sitio, LoRaWAN es más barato en el primer año. Con 1–2 Smiley conviene 4G directo en el equipo.
Validar toda la cadena en banco con hardware local, y después en una planta real, antes de comprar en volumen.
| Fase | Qué se hace | Hardware | Criterio de éxito |
|---|---|---|---|
| 1. Banco (semanas 1–2) | ChirpStack en Docker; gateway Pi + HAT SX1303; 2–3 puentes enviando votos simulados | HAT SX1303 + Pi de la flota + kits XIAO nRF52840 + Wio-SX1262 | Votos llegan al backend por MQTT; dedup por secuencia funciona |
| 2. Integración (semana 3) | Puente BLE emparejado con la tablet; la app encola y envía | Galaxy Tab A11 + puente | Voto en pantalla → dashboard en < 10 s |
| 3. Campo (semanas 4–6) | Gateway con 4G en planta piloto; Smiley en 3–5 baños | RAK7268CV2 + SIM IoT | > 99% de eventos entregados en 2 semanas; RSSI/SNR en rango |
| 4. Cierre | Informe de cobertura y costos reales; decisión de compra | — | Costo por planta confirmado |
Candidata a planta piloto: la de Coca-Cola sobre el camino a Montecristo, o el cliente ya confirmado si tiene un sitio con varios baños.
La Galaxy Tab A11 (8,7") no trae LoRa y su único USB-C queda ocupado por la alimentación, así que la radio va en un módulo aparte que la tablet maneja por Bluetooth LE. El USB-C sigue siendo solo carga y, tras un corte de luz, ambos arrancan y se reconectan solos.
| Item | Detalle |
|---|---|
| Producto | Kit XIAO nRF52840 + Wio-SX1262 (Seeed SKU 102010710) |
| Radio | Semtech SX1262, 862–930 MHz (cubre AU915); BLE 5.0 en el nRF52840 |
| Precio | ARS 44.900 lista; ARS 38.165 por transferencia (15% off) |
| Stock | La ficha indicaba 4 unidades pero mostraba aviso de sin stock: confirmar antes de pagar |
| Vendedor | InnoMakeBright, La Plata (Buenos Aires); distribuidor oficial Seeed Studio; Factura A y B |
| Contacto | WhatsApp +54 9 221 200-2344; despacho en 24 h hábiles por Andreani o Correo Argentino |
| Garantía | 15 días por defecto de fábrica en la tienda; la descripción menciona 12 meses de garantía Seeed |
| Alternativa | Módulo Wio-SX1262 suelto ARS 15.697 + XIAO nRF52840 por separado |
WD-LORA-xxxx en Bluetooth); Android guarda el vínculo.El puente recibe comandos de texto por BLE (Nordic UART Service con PIN), los encola y los envía como uplinks LoRaWAN AU915; los downlinks vuelven a la tablet. La cola persistente sigue en la tablet: borra un evento solo cuando recibe SENT.
https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json → instalar Seeed nRF52 Boards (no la versión mbed).INFO y copiar el DevEUI (ID único del chip).KEYS,<JoinEUI>,<AppKey> una vez; queda en flash y el puente hace join solo.Pines (según el variant oficial del kit en Meshtastic): NSS D4, DIO1 D1, RESET D2, BUSY D3, RXEN D5, DIO2 como switch de TX, TCXO 1,8 V.
| Comando | Respuesta | Uso |
|---|---|---|
TX,<seq>,<port>,<hex> | ACK,<seq> y luego SENT,<seq> o FAIL,<seq>,<código> | Voto o fichaje, sin confirmar |
TXC,<seq>,<port>,<hex> | SENT,<seq>,ACK o SENT,<seq>,NOACK | Latido confirmado; 3 NOACK seguidos fuerzan rejoin |
INFO | INFO,<fw>,<devEUI>,<joinEUI>,<claves>,<unido>,<subbanda> | Alta y diagnóstico |
STAT | STAT,<unido>,<cola>,<rssi>,<snr>,<uptime>,<uplinks> | Monitoreo desde la app |
KEYS,<joinEUI>,<appKey> | OK,KEYS | Aprovisionamiento |
SUBBAND,<1..8> | OK,SUBBAND y reinicio | Cambiar sub-banda |
REJOIN / REBOOT / PING | OK,REJOIN / OK,REBOOT / PONG | Mantenimiento |
| (asíncrono) | BOOT, JOINED, JOINFAIL,<c>, DL,<port>,<hex> | Eventos del puente |
Join OTAA nuevo en cada arranque (solo se guardan los DevNonce, para no gastar flash), cola en RAM de 32 mensajes, 3 s mínimo entre uplinks, watchdog de 60 s y LED rojo/azul/verde. El código no se compiló todavía contra el hardware: validarlo en la fase de banco.
/*
* WorkDone Smiley - Puente BLE <-> LoRaWAN AU915
* ------------------------------------------------
* Hardware : Seeed XIAO nRF52840 + Wio-SX1262 for XIAO (kit SKU 102010710)
* Core : "Seeed nRF52 Boards" (NO el core mbed) -> placa "Seeed XIAO nRF52840"
* URL gestor de placas: https://files.seeedstudio.com/arduino/package_seeeduino_boards_index.json
* Librerias: RadioLib >= 7.1 (Library Manager). Bluefruit y LittleFS vienen con el core.
* Flasheo : doble toque en RESET -> aparece unidad XIAO-SENSE/XIAO-BOOT -> Upload desde Arduino IDE
*
* La tablet (app Smiley) se conecta por BLE (Nordic UART Service, con PIN) y manda
* comandos de texto, una linea por comando terminada en '\n'. Los mismos comandos
* se aceptan por USB serie (115200) para pruebas de banco.
*
* Tablet -> puente
* PING -> PONG
* INFO -> INFO,<fw>,<devEUI>,<joinEUI>,<keys 0|1>,<joined 0|1>,<subband>
* KEYS,<joinEUI 16 hex>,<appKey 32 hex> -> OK,KEYS (guarda en flash y hace join de nuevo)
* SUBBAND,<1..8> -> OK,SUBBAND (guarda y reinicia)
* TX,<seq>,<port>,<hex> -> ACK,<seq> | ERR,<seq>,<motivo> (uplink sin confirmar)
* TXC,<seq>,<port>,<hex> -> ACK,<seq> | ERR,<seq>,<motivo> (uplink confirmado)
* STAT -> STAT,<joined>,<cola>,<rssi>,<snr>,<uptime_s>,<uplinks>
* REJOIN -> OK,REJOIN
* REBOOT -> OK,REBOOT
*
* Puente -> tablet (asincronico)
* BOOT,<fw>
* JOINED | JOINFAIL,<codigo> | REJOINING
* SENT,<seq> | SENT,<seq>,ACK | SENT,<seq>,NOACK | FAIL,<seq>,<codigo>
* DL,<port>,<hex>
* ERR,RADIO,<codigo> | ERR,UNKNOWN | ERR,BADARGS
*
* Diseno:
* - La cola persistente (fuente de verdad) vive en la tablet. El puente tiene una cola
* en RAM de QUEUE_SIZE mensajes; la tablet borra un evento de su cola al recibir SENT.
* - Si se corta el BLE, la tablet reenvia lo pendiente al reconectar; el backend
* deduplica por (DevEUI, secuencia de aplicacion dentro del payload).
* - En cada arranque se hace un join OTAA nuevo. Solo se persisten los DevNonce
* (una escritura de flash por intento de join), no la sesion: asi no se gasta la flash.
* - DevEUI = ID unico de fabrica del nRF52840 (FICR). Se lee con INFO y se da de alta en ChirpStack.
* - Cada MAX_MISSED_ACKS uplinks confirmados sin ACK seguidos, se fuerza un rejoin.
* - Watchdog de hardware de WDT_TIMEOUT_S segundos.
*
* LEDs (activos en bajo): rojo = sin claves o radio en error, azul = uniendose, verde = unido.
*/
#include <Arduino.h>
#include <bluefruit.h>
#include <Adafruit_LittleFS.h>
#include <InternalFileSystem.h>
#include <RadioLib.h>
using namespace Adafruit_LittleFS_Namespace;
#define FW_VERSION "1.0.0"
// ---------- Pines Wio-SX1262 para XIAO (kit 102010710) ----------
#define PIN_LORA_NSS D4
#define PIN_LORA_DIO1 D1
#define PIN_LORA_RESET D2
#define PIN_LORA_BUSY D3
#define PIN_LORA_RXEN D5
#define LORA_TCXO_V 1.8f
// ---------- Parametros ----------
#define BLE_PIN "123456" // Cambiar por lote o por cliente
#define DEFAULT_SUBBAND 2 // AU915 FSB2 (canales 8-15 + 65); debe coincidir con gateway y ChirpStack
#define QUEUE_SIZE 32
#define MAX_PAYLOAD 51
#define LINE_MAX 200
#define MIN_TX_GAP_MS 3000UL
#define MAX_TX_TRIES 3
#define WDT_TIMEOUT_S 60
#define MAX_MISSED_ACKS 3
#define JOIN_BACKOFF_MIN 15000UL
#define JOIN_BACKOFF_MAX 900000UL
static const char* CFG_PATH = "/wd_cfg.bin";
static const char* NONCES_PATH = "/wd_nonces.bin";
static const uint32_t CFG_MAGIC = 0x57444C31; // "WDL1"
struct Config {
uint32_t magic;
uint64_t joinEUI;
uint8_t appKey[16];
uint8_t subband;
uint8_t hasKeys;
};
struct TxItem {
uint32_t seq;
uint8_t port;
uint8_t len;
uint8_t tries;
bool confirmed;
uint8_t data[MAX_PAYLOAD];
};
struct LineReader {
char buf[LINE_MAX];
uint16_t len;
};
enum LedState { LED_ERROR, LED_JOINING, LED_JOINED };
// ---------- Objetos globales ----------
SX1262 radio = new Module(PIN_LORA_NSS, PIN_LORA_DIO1, PIN_LORA_RESET, PIN_LORA_BUSY);
LoRaWANNode* node = nullptr;
BLEUart bleuart(1024);
BLEDis bledis;
Config cfg;
uint64_t devEUI = 0;
bool radioOk = false;
bool joined = false;
uint32_t nextJoinAt = 0;
uint32_t joinBackoffMs = JOIN_BACKOFF_MIN;
uint32_t lastTxAt = 0;
uint32_t uplinkCount = 0;
uint8_t missedAcks = 0;
float lastRssi = 0.0f;
float lastSnr = 0.0f;
TxItem txQueue[QUEUE_SIZE];
uint8_t qHead = 0;
uint8_t qTail = 0;
uint8_t qCount = 0;
LineReader bleReader = { {0}, 0 };
LineReader serialReader = { {0}, 0 };
// ---------- Salida ----------
void out(const String& s) {
Serial.println(s);
if (Bluefruit.connected()) {
bleuart.print(s);
bleuart.print("\n");
}
}
// ---------- LEDs ----------
void setLed(LedState st) {
digitalWrite(LED_RED, st == LED_ERROR ? LOW : HIGH);
digitalWrite(LED_BLUE, st == LED_JOINING ? LOW : HIGH);
digitalWrite(LED_GREEN, st == LED_JOINED ? LOW : HIGH);
}
void refreshLed() {
if (!radioOk || !cfg.hasKeys) setLed(LED_ERROR);
else if (!joined) setLed(LED_JOINING);
else setLed(LED_JOINED);
}
// ---------- Hex ----------
int hexNibble(char c) {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'a' && c <= 'f') return c - 'a' + 10;
if (c >= 'A' && c <= 'F') return c - 'A' + 10;
return -1;
}
// Devuelve bytes decodificados, -1 si el hex es invalido, -2 si excede maxLen
int parseHex(const char* s, uint8_t* dst, int maxLen) {
int n = strlen(s);
if (n == 0 || (n % 2) != 0) return -1;
if (n / 2 > maxLen) return -2;
for (int i = 0; i < n / 2; i++) {
int h = hexNibble(s[2 * i]);
int l = hexNibble(s[2 * i + 1]);
if (h < 0 || l < 0) return -1;
dst[i] = (uint8_t)((h << 4) | l);
}
return n / 2;
}
String toHex(const uint8_t* b, size_t n) {
static const char* digits = "0123456789ABCDEF";
String s;
s.reserve(n * 2);
for (size_t i = 0; i < n; i++) {
s += digits[b[i] >> 4];
s += digits[b[i] & 0x0F];
}
return s;
}
String u64Hex(uint64_t v) {
uint8_t b[8];
for (int i = 0; i < 8; i++) b[i] = (uint8_t)((v >> (56 - 8 * i)) & 0xFF);
return toHex(b, 8);
}
bool parseU64(const char* s, uint64_t& v) {
uint8_t b[8];
if (strlen(s) != 16) return false;
if (parseHex(s, b, 8) != 8) return false;
v = 0;
for (int i = 0; i < 8; i++) v = (v << 8) | b[i];
return true;
}
// ---------- Flash (LittleFS interno) ----------
bool readFileBytes(const char* path, void* buf, size_t len) {
File f(InternalFS);
if (!f.open(path, FILE_O_READ)) return false;
int r = f.read(buf, (uint16_t)len);
f.close();
return r == (int)len;
}
bool writeFileBytes(const char* path, const void* buf, size_t len) {
InternalFS.remove(path);
File f(InternalFS);
if (!f.open(path, FILE_O_WRITE)) return false;
size_t w = f.write((const uint8_t*)buf, len);
f.close();
return w == len;
}
void loadConfig() {
if (!readFileBytes(CFG_PATH, &cfg, sizeof(cfg)) || cfg.magic != CFG_MAGIC) {
memset(&cfg, 0, sizeof(cfg));
cfg.magic = CFG_MAGIC;
cfg.subband = DEFAULT_SUBBAND;
cfg.hasKeys = 0;
}
if (cfg.subband < 1 || cfg.subband > 8) cfg.subband = DEFAULT_SUBBAND;
}
bool saveConfig() {
return writeFileBytes(CFG_PATH, &cfg, sizeof(cfg));
}
// ---------- Watchdog ----------
void wdtStart() {
NRF_WDT->CONFIG = (WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos) |
(WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos);
NRF_WDT->CRV = WDT_TIMEOUT_S * 32768UL;
NRF_WDT->RREN = WDT_RREN_RR0_Msk;
NRF_WDT->TASKS_START = 1;
}
void wdtFeed() {
NRF_WDT->RR[0] = WDT_RR_RR_Reload;
}
// ---------- Cola ----------
bool enqueue(const TxItem& it) {
if (qCount >= QUEUE_SIZE) return false;
txQueue[qTail] = it;
qTail = (qTail + 1) % QUEUE_SIZE;
qCount++;
return true;
}
void dequeue() {
if (qCount == 0) return;
qHead = (qHead + 1) % QUEUE_SIZE;
qCount--;
}
// ---------- Radio / LoRaWAN ----------
bool radioInit() {
radio.setRfSwitchPins(PIN_LORA_RXEN, RADIOLIB_NC);
int16_t st = radio.begin(915.0, 125.0, 9, 7, RADIOLIB_SX126X_SYNC_WORD_PRIVATE, 10, 8, LORA_TCXO_V, false);
if (st != RADIOLIB_ERR_NONE) {
out("ERR,RADIO," + String(st));
return false;
}
radio.setDio2AsRfSwitch(true);
return true;
}
void scheduleRejoin(bool immediate) {
joined = false;
missedAcks = 0;
if (immediate) {
joinBackoffMs = JOIN_BACKOFF_MIN;
nextJoinAt = millis();
}
refreshLed();
}
void tryJoin() {
if (!radioOk || !cfg.hasKeys) return;
if (node) {
delete node;
node = nullptr;
}
node = new LoRaWANNode(&radio, &AU915, cfg.subband);
node->beginOTAA(cfg.joinEUI, devEUI, cfg.appKey, cfg.appKey); // LoRaWAN 1.0.x: misma clave
uint8_t nonces[RADIOLIB_LORAWAN_NONCES_BUF_SIZE];
if (readFileBytes(NONCES_PATH, nonces, sizeof(nonces))) {
node->setBufferNonces(nonces); // si no corresponde a estas claves, RadioLib lo descarta
}
refreshLed();
wdtFeed();
int16_t st = node->activateOTAA();
wdtFeed();
memcpy(nonces, node->getBufferNonces(), sizeof(nonces));
writeFileBytes(NONCES_PATH, nonces, sizeof(nonces));
if (st == RADIOLIB_LORAWAN_NEW_SESSION || st == RADIOLIB_LORAWAN_SESSION_RESTORED) {
joined = true;
missedAcks = 0;
joinBackoffMs = JOIN_BACKOFF_MIN;
out("JOINED");
} else {
joined = false;
out("JOINFAIL," + String(st));
nextJoinAt = millis() + joinBackoffMs;
joinBackoffMs = min(joinBackoffMs * 2, JOIN_BACKOFF_MAX);
}
refreshLed();
}
void processQueue() {
if (!joined || qCount == 0 || node == nullptr) return;
if (millis() - lastTxAt < MIN_TX_GAP_MS) return;
if (node->timeUntilUplink() > 0) return;
TxItem& it = txQueue[qHead];
uint8_t down[256];
size_t downLen = 0;
LoRaWANEvent_t evUp;
LoRaWANEvent_t evDown;
wdtFeed();
int16_t st = node->sendReceive(it.data, it.len, it.port, down, &downLen, it.confirmed, &evUp, &evDown);
wdtFeed();
lastTxAt = millis();
if (st < RADIOLIB_ERR_NONE) {
it.tries++;
if (st == RADIOLIB_ERR_PACKET_TOO_LONG || it.tries >= MAX_TX_TRIES) {
out("FAIL," + String(it.seq) + "," + String(st));
dequeue();
}
return;
}
uplinkCount++;
if (st > 0) {
lastRssi = radio.getRSSI();
lastSnr = radio.getSNR();
if (downLen > 0) {
out("DL," + String(evDown.fPort) + "," + toHex(down, downLen));
}
}
if (it.confirmed) {
if (st > 0 && evDown.confirming) {
missedAcks = 0;
out("SENT," + String(it.seq) + ",ACK");
} else {
missedAcks++;
out("SENT," + String(it.seq) + ",NOACK");
if (missedAcks >= MAX_MISSED_ACKS) {
out("REJOINING");
scheduleRejoin(true);
}
}
} else {
out("SENT," + String(it.seq));
}
dequeue();
}
// ---------- Comandos ----------
void handleTx(bool confirmed) {
char* seqS = strtok(NULL, ",");
char* portS = strtok(NULL, ",");
char* hexS = strtok(NULL, ",");
if (!seqS || !portS || !hexS) {
out("ERR,BADARGS");
return;
}
uint32_t seq = strtoul(seqS, NULL, 10);
int port = atoi(portS);
if (port < 1 || port > 223) {
out("ERR," + String(seq) + ",BADPORT");
return;
}
TxItem it;
memset(&it, 0, sizeof(it));
int len = parseHex(hexS, it.data, MAX_PAYLOAD);
if (len == -2) {
out("ERR," + String(seq) + ",TOOLONG");
return;
}
if (len < 1) {
out("ERR," + String(seq) + ",BADHEX");
return;
}
it.seq = seq;
it.port = (uint8_t)port;
it.len = (uint8_t)len;
it.tries = 0;
it.confirmed = confirmed;
if (!enqueue(it)) {
out("ERR," + String(seq) + ",FULL");
return;
}
out("ACK," + String(seq));
}
void handleKeys() {
char* joinS = strtok(NULL, ",");
char* keyS = strtok(NULL, ",");
if (!joinS || !keyS) {
out("ERR,BADARGS");
return;
}
uint64_t j = 0;
uint8_t key[16];
if (!parseU64(joinS, j) || strlen(keyS) != 32 || parseHex(keyS, key, 16) != 16) {
out("ERR,BADARGS");
return;
}
cfg.joinEUI = j;
memcpy(cfg.appKey, key, 16);
cfg.hasKeys = 1;
if (!saveConfig()) {
out("ERR,FLASH");
return;
}
out("OK,KEYS");
scheduleRejoin(true);
}
void handleSubband() {
char* nS = strtok(NULL, ",");
if (!nS) {
out("ERR,BADARGS");
return;
}
int n = atoi(nS);
if (n < 1 || n > 8) {
out("ERR,BADARGS");
return;
}
cfg.subband = (uint8_t)n;
if (!saveConfig()) {
out("ERR,FLASH");
return;
}
out("OK,SUBBAND");
delay(300);
NVIC_SystemReset();
}
void handleLine(char* line) {
char* cmd = strtok(line, ",");
if (!cmd) return;
if (!strcmp(cmd, "PING")) {
out("PONG");
} else if (!strcmp(cmd, "INFO")) {
out(String("INFO,") + FW_VERSION + "," + u64Hex(devEUI) + "," + u64Hex(cfg.joinEUI) + "," +
(cfg.hasKeys ? "1" : "0") + "," + (joined ? "1" : "0") + "," + String(cfg.subband));
} else if (!strcmp(cmd, "STAT")) {
out("STAT," + String(joined ? 1 : 0) + "," + String(qCount) + "," + String(lastRssi, 1) + "," +
String(lastSnr, 1) + "," + String(millis() / 1000) + "," + String(uplinkCount));
} else if (!strcmp(cmd, "TX")) {
handleTx(false);
} else if (!strcmp(cmd, "TXC")) {
handleTx(true);
} else if (!strcmp(cmd, "KEYS")) {
handleKeys();
} else if (!strcmp(cmd, "SUBBAND")) {
handleSubband();
} else if (!strcmp(cmd, "REJOIN")) {
out("OK,REJOIN");
scheduleRejoin(true);
} else if (!strcmp(cmd, "REBOOT")) {
out("OK,REBOOT");
delay(300);
NVIC_SystemReset();
} else {
out("ERR,UNKNOWN");
}
}
void feedReader(LineReader& r, char c) {
if (c == '\r') return;
if (c == '\n') {
r.buf[r.len] = 0;
if (r.len > 0) handleLine(r.buf);
r.len = 0;
return;
}
if (r.len < LINE_MAX - 1) {
r.buf[r.len++] = c;
} else {
r.len = 0; // linea demasiado larga: se descarta
}
}
void pollInputs() {
while (bleuart.available()) feedReader(bleReader, (char)bleuart.read());
while (Serial.available()) feedReader(serialReader, (char)Serial.read());
}
// ---------- BLE ----------
void connectCallback(uint16_t connHandle) {
(void)connHandle;
Serial.println("BLE conectado");
}
void disconnectCallback(uint16_t connHandle, uint8_t reason) {
(void)connHandle;
Serial.println("BLE desconectado, motivo 0x" + String(reason, HEX));
}
void bleInit() {
Bluefruit.configPrphBandwidth(BANDWIDTH_MAX);
Bluefruit.begin();
Bluefruit.setTxPower(4);
String name = "WD-LORA-" + u64Hex(devEUI).substring(12);
Bluefruit.setName(name.c_str());
Bluefruit.Security.setPIN(BLE_PIN);
Bluefruit.Periph.setConnectCallback(connectCallback);
Bluefruit.Periph.setDisconnectCallback(disconnectCallback);
bledis.setManufacturer("LaEmpresa");
bledis.setModel("WorkDone Smiley LoRa Bridge");
bledis.setFirmwareRev(FW_VERSION);
bledis.begin();
bleuart.setPermission(SECMODE_ENC_WITH_MITM, SECMODE_ENC_WITH_MITM);
bleuart.begin();
Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
Bluefruit.Advertising.addTxPower();
Bluefruit.Advertising.addService(bleuart);
Bluefruit.ScanResponse.addName();
Bluefruit.Advertising.restartOnDisconnect(true);
Bluefruit.Advertising.setInterval(32, 244);
Bluefruit.Advertising.setFastTimeout(30);
Bluefruit.Advertising.start(0);
}
// ---------- Arduino ----------
void setup() {
pinMode(LED_RED, OUTPUT);
pinMode(LED_GREEN, OUTPUT);
pinMode(LED_BLUE, OUTPUT);
setLed(LED_ERROR);
Serial.begin(115200);
uint32_t t0 = millis();
while (!Serial && millis() - t0 < 1500) delay(10);
devEUI = ((uint64_t)NRF_FICR->DEVICEID[1] << 32) | (uint64_t)NRF_FICR->DEVICEID[0];
InternalFS.begin();
loadConfig();
bleInit();
radioOk = radioInit();
wdtStart();
out(String("BOOT,") + FW_VERSION);
out("INFO," + String(FW_VERSION) + "," + u64Hex(devEUI) + "," + u64Hex(cfg.joinEUI) + "," +
(cfg.hasKeys ? "1" : "0") + ",0," + String(cfg.subband));
nextJoinAt = millis() + 2000;
refreshLed();
}
void loop() {
wdtFeed();
pollInputs();
if (!joined && radioOk && cfg.hasKeys && (int32_t)(millis() - nextJoinAt) >= 0) {
tryJoin();
}
processQueue();
delay(5);
}
| Riesgo | Impacto | Mitigación |
|---|---|---|
| Mala señal 4G en el punto del gateway | Sin backhaul, eventos demorados | Medir antes; antena LTE externa; buffer SD del gateway; cola local en cada Smiley |
| Cobertura LoRa insuficiente en naves grandes | Baños sin enlace | Prueba de campo; segundo gateway |
| Corte de energía | Gateway caído | UPS chica o PoE desde un switch con UPS |
| Sub-banda mal configurada | Nodos que no se unen | Plantilla única para gateway, ChirpStack y firmware |
| Módulo USB comprado que resulta solo P2P | No habla con el gateway | Usar puente XIAO, Wio-E5 mini o RAK3172 (LoRaWAN confirmado) |