Objetivo y caso de uso
Qué construirás: Un nodo sensor de bajo consumo para monitoreo ambiental utilizando TTGO LoRa32, BME280 y ADS1115.
Para qué sirve
- Monitoreo de temperatura y humedad en invernaderos usando BME280.
- Medición de niveles de agua en cultivos mediante ADS1115.
- Transmisión de datos de sensores a través de LoRa para largas distancias.
- Integración con sistemas de alerta temprana para condiciones ambientales adversas.
Resultado esperado
- Datos de temperatura y humedad reportados cada 10 minutos.
- Latencia de transmisión de datos inferior a 5 segundos.
- Consumo de energía del nodo menor a 50 mA en modo activo.
- Rango de transmisión efectivo de al menos 5 km en campo abierto.
Público objetivo: Ingenieros y desarrolladores en IoT; Nivel: Avanzado
Arquitectura/flujo: Nodo sensor LoRa -> Gateway LoRa -> Plataforma de monitoreo en la nube.
Nivel: Avanzado
Prerrequisitos
- Sistemas operativos verificados
- Windows 11 Pro 23H2 (con privilegios de administrador para instalar drivers)
- Ubuntu 22.04.4 LTS (usuario en grupo dialout)
-
macOS 14 Sonoma
-
Toolchain exacta (probada)
- PlatformIO Core 6.1.14 (CLI)
- Python 3.11.6
- Git 2.44.0
- PlatformIO platform espressif32@6.5.0
- Framework Arduino-ESP32 2.0.14 (resuelto por espressif32@6.5.0)
- toolchain-xtensa-esp32 gcc 8.4.0 (instalado por la plataforma anterior)
- esptool.py 4.6 (tool-esptoolpy suministrado por PlatformIO)
-
Librerías Arduino (versiones fijas en este proyecto)
- sandeepmistry/LoRa@0.8.0
- adafruit/Adafruit BME280 Library@2.2.4
- adafruit/Adafruit Unified Sensor@1.1.14
- adafruit/Adafruit ADS1X15@2.4.0
-
Drivers de USB–Serie
- La TTGO LoRa32 emplea normalmente CP2102/CP2104 (Silicon Labs). Instalar el driver:
- Windows/macOS: descargar de Silicon Labs “CP210x Universal Windows/Mac Driver”.
- Linux: el kernel ya incluye cp210x. Añade tu usuario a dialout: sudo usermod -aG dialout $USER y reinicia sesión.
-
Si tu variante usase CH340/CH34x, instala el driver de WCH.
-
Notas de conformidad LoRa
- Selecciona banda según tu región y normativa:
- EU868: 868.1/868.3/868.5 MHz (duty-cycle 1% típico).
- US915: 903.9–927.5 MHz (canales espaciados 0.2 MHz).
- En este caso práctico se configura por defecto EU868 (868.1 MHz); puedes cambiarlo en build_flags.
Materiales
- 1× TTGO LoRa32 (ESP32 + SX1276) con interfaz USB (modelo de LILYGO, también llamado “TTGO LoRa32 v1/v2”).
- 1× BME280 (módulo I2C, dirección por defecto 0x76/0x77).
- 1× ADS1115 (módulo I2C, dirección por defecto 0x48).
- 1× Sensor capacitivo de humedad de suelo 3.3 V (salida analógica, no el resistivo).
- 1× Batería LiPo 3.7 V (con conector JST-PH compatible con la TTGO LoRa32).
- Resistencias para divisor de tensión de batería hacia ADS1115 (ejemplo: 100 kΩ + 100 kΩ).
- Cables Dupont Macho–Hembra/Macho–Macho.
- Cable USB-C o Micro-USB según tu TTGO LoRa32.
- Opcional, para validación por radio:
- 1× segunda TTGO LoRa32 (ESP32 + SX1276) para actuar como receptor simple.
Notas:
– El objetivo “lora-agro-telemetry-lowpower” exige minimizar consumo: usaremos deep sleep del ESP32, el SX1276 en sleep entre tramas, BME280 en modo forced y lecturas single-shot del ADS1115.
Preparación y conexión
La TTGO LoRa32 integra el SX1276 y el bus SPI ya cableado en la propia placa. Los sensores BME280 y ADS1115 se conectan por I2C. Para sensar humedad de suelo usando un sensor capacitivo, su salida analógica se conectará a la entrada A0 del ADS1115. Si se desea telemetría de batería, llevaremos la tensión VBAT a A1 del ADS1115 a través de un divisor 1:2 (por ejemplo, 100 kΩ/100 kΩ), de modo que un máximo de 4.2 V quede por debajo del rango seguro del ADC externo.
Tabla de pines, buses y direcciones
| Componente/Señal | TTGO LoRa32 (ESP32) | Notas/Detalle |
|---|---|---|
| I2C SDA | GPIO 21 | Conectar a SDA de BME280 y ADS1115 |
| I2C SCL | GPIO 22 | Conectar a SCL de BME280 y ADS1115 |
| 3V3 | 3V3 | Alimentación sensores BME280, ADS1115 y sensor de suelo (si admite 3.3 V) |
| GND | GND | Tierra común para todos los módulos |
| SX1276 NSS (SS) | GPIO 18 | Interno a la placa; lo fijamos en el código LoRa.setPins(18,14,26) |
| SX1276 RST | GPIO 14 | Interno |
| SX1276 DIO0 | GPIO 26 | Interno |
| SPI SCK | GPIO 5 | Interno al SX1276 |
| SPI MISO | GPIO 19 | Interno al SX1276 |
| SPI MOSI | GPIO 27 | Interno al SX1276 |
| ADS1115 dirección I2C | 0x48 | Por defecto (ADDR a GND) |
| BME280 dirección I2C | 0x76 o 0x77 | Según módulo (normalmente 0x76) |
| Sensor suelo -> ADS1115 A0 | ADS1115 A0 | Señal analógica del sensor capacitivo |
| VBAT dividido -> ADS1115 A1 | ADS1115 A1 | Divisor 100 kΩ/100 kΩ entre VBAT y GND; punto medio a A1 |
Conexiones paso a paso:
1. Alimentación y bus I2C:
– TTGO 3V3 → 3V3 de BME280, 3V3 de ADS1115 y VCC del sensor de suelo.
– TTGO GND → GND de BME280, GND de ADS1115 y GND del sensor de suelo.
– TTGO GPIO21 (SDA) → SDA de BME280 y SDA de ADS1115.
– TTGO GPIO22 (SCL) → SCL de BME280 y SCL de ADS1115.
2. Entradas analógicas en ADS1115:
– Sensor suelo OUT → ADS1115 A0.
– Punto medio del divisor de VBAT (entre 100 kΩ y 100 kΩ) → ADS1115 A1.
– Extremos del divisor: superior a VBAT (línea de batería de la TTGO, no al 5 V), inferior a GND.
3. Batería:
– Conecta la LiPo al conector JST de la TTGO LoRa32. La placa integra carga/gestión básica.
4. Antena:
– Conecta una antena adecuada a la TTGO (imprescindible para no dañar el SX1276).
Verificación inicial (opcional pero recomendable):
– Con un escáner I2C se deben ver 0x48 (ADS1115) y 0x76/0x77 (BME280).
Código completo
A continuación se muestra un proyecto completo para PlatformIO (framework Arduino). Implementa:
– Lectura de BME280 en modo forced (reduce consumo).
– Lectura single-shot de ADS1115 en A0 (humedad de suelo) y A1 (VBAT/2).
– Trama binaria compacta con CRC16-IBM.
– SX1276 dormido entre tramas; ESP32 en deep sleep con temporizador.
– Frecuencia, SF y TX power configurables por build_flags.
– Desactivación de WiFi/BT.
platformio.ini
Crea el proyecto y pega este contenido en platformio.ini:
; Proyecto: lora-agro-telemetry-lowpower
; Dispositivo: TTGO LoRa32 (ESP32 + SX1276) + BME280 + ADS1115
; Toolchain exacta fijada vía platform y libs
[env:ttgo-lora32-v1]
platform = espressif32@6.5.0
board = ttgo-lora32-v1
framework = arduino
; Paquetes de plataforma (se instalan automáticamente con espressif32@6.5.0)
; - toolchain-xtensa-esp32 (gcc 8.4.0)
; - tool-esptoolpy (esptool.py 4.6)
; - framework-arduinoespressif32 (Arduino-ESP32 2.0.14)
monitor_speed = 115200
monitor_filters = time, esp32_exception_decoder
lib_deps =
sandeepmistry/LoRa@0.8.0
adafruit/Adafruit BME280 Library@2.2.4
adafruit/Adafruit Unified Sensor@1.1.14
adafruit/Adafruit ADS1X15@2.4.0
; Parámetros de radio y hardware (modificables)
build_flags =
-D LORA_FREQ_MHZ=868.1
-D LORA_SF=7
-D LORA_TX_PWR_DBM=14
-D I2C_SDA=21
-D I2C_SCL=22
-D SOIL_CH=0
-D VBAT_CH=1
-D WAKE_INTERVAL_S=900
-D REGION_EU868
Notas:
– Para US915, cambia LORA_FREQ_MHZ=915.0 y retira REGION_EU868 o define REGION_US915.
– Puedes afinar la SF (7–12) y potencia (2–20 dBm) según normativa y enlace.
src/main.cpp
Crea src/main.cpp con el siguiente código. Está documentado por bloques clave.
#include <Arduino.h>
#include <Wire.h>
#include <SPI.h>
#include <LoRa.h>
#include <WiFi.h>
#include "esp_bt.h"
#include <Adafruit_BME280.h>
#include <Adafruit_ADS1X15.h>
#ifndef LORA_FREQ_MHZ
#define LORA_FREQ_MHZ 868.1
#endif
#ifndef LORA_SF
#define LORA_SF 7
#endif
#ifndef LORA_TX_PWR_DBM
#define LORA_TX_PWR_DBM 14
#endif
#ifndef I2C_SDA
#define I2C_SDA 21
#endif
#ifndef I2C_SCL
#define I2C_SCL 22
#endif
#ifndef WAKE_INTERVAL_S
#define WAKE_INTERVAL_S 900
#endif
#ifndef SOIL_CH
#define SOIL_CH 0
#endif
#ifndef VBAT_CH
#define VBAT_CH 1
#endif
// Mapeo SX1276 en TTGO LoRa32
static constexpr int LORA_SS = 18;
static constexpr int LORA_RST = 14;
static constexpr int LORA_DIO0 = 26;
// SPI (VSPI) ya mapeado a SCK=5, MISO=19, MOSI=27 por defecto en ESP32-ARDUINO
// Sensores I2C
Adafruit_BME280 bme;
Adafruit_ADS1115 ads;
// Contador persistente en deep sleep
RTC_DATA_ATTR uint32_t rtc_seq = 0;
// CRC16-IBM (polinomio 0xA001)
uint16_t crc16_ibm(const uint8_t* data, size_t len) {
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < len; ++i) {
crc ^= data[i];
for (uint8_t b = 0; b < 8; ++b) {
if (crc & 1) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc;
}
// Estructura de trama compacta
#pragma pack(push, 1)
struct FrameV1 {
uint8_t pre[2]; // 'A','G'
uint8_t ver; // 0x01
uint32_t dev; // 32 bits del MAC (ID)
uint32_t seq; // contador
int16_t t_c_x100; // temperatura *100 (°C)
uint16_t rh_x100; // humedad *100 (%)
uint16_t press_hPa_x10; // presión *10 (hPa)
uint16_t soil_permille; // humedad suelo en ‰ (0-1000)
uint16_t vbat_mV; // batería en mV
uint16_t crc; // CRC16-IBM sobre cabecera+datos
};
#pragma pack(pop)
// Lectura BME280 en modo de baja energía (forced)
bool read_bme280(float &tC, float &rh, float &p_hPa) {
// Configurar I2C si no está iniciado
static bool wireBegun = false;
if (!wireBegun) {
Wire.begin(I2C_SDA, I2C_SCL);
wireBegun = true;
}
static bool bmeInit = false;
if (!bmeInit) {
// Probar 0x76 y 0x77
if (!bme.begin(0x76) && !bme.begin(0x77)) {
return false;
}
// Configuración mínima: oversampling x1, filtro off, modo FORCED
bme.setSampling(
Adafruit_BME280::MODE_FORCED,
Adafruit_BME280::SAMPLE_X1, // temp
Adafruit_BME280::SAMPLE_X1, // hum
Adafruit_BME280::SAMPLE_X1, // pres
Adafruit_BME280::FILTER_OFF
);
bmeInit = true;
}
// Disparo de conversión en modo FORCED
bme.takeForcedMeasurement();
tC = bme.readTemperature(); // °C
rh = bme.readHumidity(); // %
p_hPa = bme.readPressure() / 100.0F; // hPa
return !(isnan(tC) || isnan(rh) || isnan(p_hPa));
}
// Inicialización ADS1115 en modo lectura single-shot
bool init_ads1115() {
static bool init = false;
if (!init) {
if (!ads.begin(0x48)) return false;
ads.setGain(GAIN_ONE); // ±4.096 V -> 0.125 mV/LSB
init = true;
}
return true;
}
float read_ads_voltage(uint8_t ch) {
int16_t counts = ads.readADC_SingleEnded(ch);
return ads.computeVolts(counts); // Volts referidos a Vdd ADC
}
// Calibración simple para humedad de suelo (ajusta a tu sensor)
uint16_t soil_percent_permille_from_voltage(float v) {
// Ajusta límites según calibración empírica de tu sonda
// Ejemplo típico de sensor capacitivo: ~0.5 V seco, ~2.5 V muy húmedo
const float v_dry = 0.5f;
const float v_wet = 2.5f;
float f = (v - v_dry) / (v_wet - v_dry);
if (f < 0.0f) f = 0.0f;
if (f > 1.0f) f = 1.0f;
return (uint16_t)lroundf(f * 1000.0f); // ‰ (0–1000)
}
// ID de dispositivo a partir del MAC
uint32_t device_id32() {
uint64_t mac = ESP.getEfuseMac();
return (uint32_t)((mac >> 16) & 0xFFFFFFFF);
}
// Inicialización de la radio LoRa con parámetros de bajo consumo
bool lora_radio_begin(double freqMHz, int sf, int txPower) {
// Apaga radios del SoC para ahorrar
WiFi.mode(WIFI_OFF);
btStop();
SPI.begin(); // VSPI por defecto
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(freqMHz * 1e6)) {
return false;
}
LoRa.setSPIFrequency(8E6);
LoRa.setTxPower(txPower); // dBm (máx. 20 con PA_BOOST, cumple normativa local)
LoRa.setSpreadingFactor(sf); // 7..12
LoRa.setSignalBandwidth(125E3); // 125 kHz (estándar)
LoRa.setCodingRate4(5); // 4/5
LoRa.setPreambleLength(8); // preámbulo estándar
LoRa.enableCrc(); // CRC de payload de la librería (además del nuestro)
return true;
}
void lora_radio_sleep() {
LoRa.idle();
LoRa.sleep(); // SX1276 en sleep (consumo mínimo)
}
// Empaquetado y envío de trama binaria
bool send_frame_once(FrameV1 &frm) {
// Calcula CRC sobre todo excepto el campo CRC
frm.crc = 0;
uint16_t crc = crc16_ibm(reinterpret_cast<const uint8_t*>(&frm), sizeof(frm) - sizeof(frm.crc));
frm.crc = crc;
// Emisión
LoRa.beginPacket();
LoRa.write(reinterpret_cast<const uint8_t*>(&frm), sizeof(frm));
int err = LoRa.endPacket(true); // true = TX async (mejor para no bloquear)
// Espera a que termine
LoRa.idle();
return (err == 1);
}
void setup() {
Serial.begin(115200);
delay(50);
Serial.println();
Serial.println(F("[lora-agro-telemetry-lowpower] Boot"));
// Lógica de secuencia
rtc_seq++;
// I2C
Wire.begin(I2C_SDA, I2C_SCL);
if (!init_ads1115()) {
Serial.println(F("ERR: ADS1115 no detectado en 0x48"));
}
// Sensores
float tC=0, rh=0, p_hPa=0;
bool bme_ok = read_bme280(tC, rh, p_hPa);
if (!bme_ok) {
Serial.println(F("ERR: BME280 no detectado (0x76/0x77)"));
}
// Lecturas ADS1115
float v_soil = read_ads_voltage(SOIL_CH);
float v_bat_div = read_ads_voltage(VBAT_CH); // miden VBAT/2 por divisor 100k/100k
float vbat = v_bat_div * 2.0f; // Volts reales
// Conversión a unidades compactas
uint16_t soil_permille = soil_percent_permille_from_voltage(v_soil);
int16_t t_c_x100 = (int16_t)lroundf(tC * 100.0f);
uint16_t rh_x100 = (uint16_t)lroundf(rh * 100.0f);
uint16_t press_hPa_x10 = (uint16_t)lroundf(p_hPa * 10.0f);
uint16_t vbat_mV = (uint16_t)lroundf(vbat * 1000.0f);
// Construcción de la trama
FrameV1 frm{};
frm.pre[0] = 'A'; frm.pre[1] = 'G';
frm.ver = 0x01;
frm.dev = device_id32();
frm.seq = rtc_seq;
frm.t_c_x100 = t_c_x100;
frm.rh_x100 = rh_x100;
frm.press_hPa_x10 = press_hPa_x10;
frm.soil_permille = soil_permille;
frm.vbat_mV = vbat_mV;
Serial.printf("ID=0x%08lX SEQ=%lu T=%.2fC RH=%.2f%% P=%.1fhPa SOIL=%.1f%% VBAT=%.3fV\n",
(unsigned long)frm.dev, (unsigned long)frm.seq,
tC, rh, p_hPa, soil_permille/10.0f, vbat);
// Radio
if (!lora_radio_begin(LORA_FREQ_MHZ, LORA_SF, LORA_TX_PWR_DBM)) {
Serial.println(F("ERR: Fallo init LoRa (frecuencia/pines/región)"));
} else {
bool ok = send_frame_once(frm);
Serial.printf("TX %s, len=%u, CRC=0x%04X, F=%.1fMHz SF=%d PWR=%ddBm\n",
ok ? "OK" : "ERR", (unsigned)sizeof(frm), frm.crc, LORA_FREQ_MHZ, LORA_SF, LORA_TX_PWR_DBM);
}
// Duerme la radio
lora_radio_sleep();
// Espera corta para drenar UART
delay(50);
// Programa deep sleep con temporizador
esp_sleep_enable_timer_wakeup((uint64_t)WAKE_INTERVAL_S * 1000000ULL);
Serial.printf("DeepSleep %us...\n", WAKE_INTERVAL_S);
Serial.flush();
esp_deep_sleep_start();
}
void loop() {
// No se usa; el ESP32 nunca entra aquí debido a deep sleep
}
Puntos clave del código:
– Se desconectan WiFi y BT para reducir consumo.
– El BME280 se usa en modo forced, volviendo a sleep tras la medición.
– El ADS1115 realiza lecturas single-shot, que apagan internamente el conversor entre muestras.
– La trama “AG v1” incluye un CRC16-IBM propio y además se habilita el CRC de la librería LoRa.
– Se usa RTC_DATA_ATTR para mantener el contador de secuencia entre ciclos de deep sleep.
– El SX1276 se pone en sleep tras el envío.
– El ESP32 entra en deep sleep durante WAKE_INTERVAL_S (por defecto 900 s = 15 min).
Receptor de validación (opcional, misma TTGO LoRa32)
Para validar por aire, flashea en otra TTGO LoRa32 el siguiente receptor de propósito general. Solo decodifica cabecera y CRC para mostrar los valores.
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>
static constexpr int LORA_SS = 18;
static constexpr int LORA_RST = 14;
static constexpr int LORA_DIO0 = 26;
#ifndef LORA_FREQ_MHZ
#define LORA_FREQ_MHZ 868.1
#endif
#ifndef LORA_SF
#define LORA_SF 7
#endif
#pragma pack(push,1)
struct FrameV1 {
uint8_t pre[2];
uint8_t ver;
uint32_t dev;
uint32_t seq;
int16_t t_c_x100;
uint16_t rh_x100;
uint16_t press_hPa_x10;
uint16_t soil_permille;
uint16_t vbat_mV;
uint16_t crc;
};
#pragma pack(pop)
uint16_t crc16_ibm(const uint8_t* d, size_t n) {
uint16_t crc=0xFFFF;
for(size_t i=0;i<n;i++){ crc^=d[i]; for(uint8_t b=0;b<8;b++){ crc=(crc&1)?(crc>>1)^0xA001:(crc>>1);} }
return crc;
}
void setup() {
Serial.begin(115200);
SPI.begin();
LoRa.setPins(LORA_SS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(LORA_FREQ_MHZ*1e6)) {
Serial.println("ERR: LoRa.begin()");
while(1) delay(1000);
}
LoRa.setSpreadingFactor(LORA_SF);
LoRa.setSignalBandwidth(125E3);
LoRa.setCodingRate4(5);
LoRa.enableCrc();
Serial.println("RX listo");
}
void loop() {
int packetSize = LoRa.parsePacket();
if (packetSize == (int)sizeof(FrameV1)) {
FrameV1 frm{};
LoRa.readBytes((uint8_t*)&frm, sizeof(frm));
uint16_t crc = frm.crc; frm.crc = 0;
uint16_t calc = crc16_ibm((uint8_t*)&frm, sizeof(frm)-2);
if (crc == calc && frm.pre[0]=='A' && frm.pre[1]=='G' && frm.ver==0x01) {
float tC = frm.t_c_x100 / 100.0f;
float rh = frm.rh_x100 / 100.0f;
float p = frm.press_hPa_x10 / 10.0f;
float soilPct = frm.soil_permille / 10.0f;
float vbat = frm.vbat_mV / 1000.0f;
Serial.printf("OK dev=0x%08lX seq=%lu T=%.2fC RH=%.2f%% P=%.1fhPa SOIL=%.1f%% VBAT=%.3fV RSSI=%d SNR=%.1f\n",
(unsigned long)frm.dev, (unsigned long)frm.seq, tC, rh, p, soilPct, vbat, LoRa.packetRssi(), LoRa.packetSnr());
} else {
Serial.println("ERR CRC/cabecera");
}
}
}
Compilación, flash y ejecución
A continuación los comandos exactos con PlatformIO CLI. Puedes usarlos en cualquier SO con la consola/terminal.
1) Verificar versiones de Python y PlatformIO:
python --version
pio --version
2) Crear el proyecto (en una carpeta nueva):
mkdir -p ~/proyectos/lora-agro-telemetry-lowpower
cd ~/proyectos/lora-agro-telemetry-lowpower
pio project init --board ttgo-lora32-v1
3) Sustituye platformio.ini por el proporcionado en este caso práctico y crea src/main.cpp con el código anterior.
4) Opcional: listar la placa para confirmar:
pio boards | grep -i ttgo
5) Actualizar paquetes y compilar:
pio pkg update
pio run -t clean
pio run
6) Conecta la TTGO LoRa32 por USB. Identifica el puerto:
– Windows: comprueba en el Administrador de dispositivos (COMx).
– Linux: dmesg | grep -i ttyUSB y ls -l /dev/ttyUSB*
– macOS: ls -l /dev/tty.SLAB_USBtoUART
7) Flashear el firmware:
pio run -t upload
8) Abrir monitor serie a 115200 baudios:
pio device monitor --baud 115200 --echo --filter time --filter esp32_exception_decoder
Notas específicas por SO:
– Linux udev: si no tienes permisos, crea /etc/udev/rules.d/99-usb-serial.rules con:
– SUBSYSTEM==»tty», ATTRS{idVendor}==»10c4″, ATTRS{idProduct}==»ea60″, MODE=»0666″, GROUP=»dialout»
– Luego: sudo udevadm control –reload-rules && sudo udevadm trigger
– Windows: si upload falla intermitente, prueba cables USB de calidad y puertos traseros.
Para el receptor opcional, crea otro proyecto (o un environment aparte) y repite los pasos con su main.cpp.
Validación paso a paso
1) Validación de consola del transmisor:
– Tras el reset, deberías ver líneas similares a:
– [lora-agro-telemetry-lowpower] Boot
– ID=0x12AB34CD SEQ=1 T=23.12C RH=45.67% P=1013.2hPa SOIL=56.7% VBAT=4.051V
– TX OK, len=21, CRC=0x5A93, F=868.1MHz SF=7 PWR=14dBm
– DeepSleep 900s…
- SEQ aumentará en cada despertar (1,2,3…).
2) Validación de sensores:
– Si el BME280 no está o su dirección no coincide, verás ERR: BME280 no detectado.
– Si el ADS1115 no responde, verás ERR: ADS1115 no detectado en 0x48.
– Acerca un dedo al sensor de suelo (si está al aire) o insértalo en tierra húmeda; la lectura SOIL deberá aumentar.
3) Validación de radio con receptor (opcional, segunda TTGO LoRa32):
– En el receptor, al abrir el monitor serie deberías ver:
– RX listo
– OK dev=0x12AB34CD seq=1 T=23.12C RH=45.67% P=1013.2hPa SOIL=56.7% VBAT=4.051V RSSI=-72 SNR=9.8
– Comprueba que el dev (ID) y seq coinciden con el transmisor y que los valores son razonables.
4) Validación de consumo (orientativa):
– Con un medidor USB en línea, observa tres fases:
– Pico al transmitir LoRa (decenas de mA, típico 80–120 mA por milisegundos).
– Activo durante sensado y TX (20–70 mA según placa).
– Deep sleep: debería caer significativamente. En TTGO LoRa32, dependiendo del regulador y del SX1276, es habitual ver cientos de µA a ~1–2 mA. Registra el valor.
– Si el consumo en deep sleep no baja de ~5 mA, revisa:
– Que WiFi/BT estén apagados.
– Que no tengas LEDs siempre encendidos.
– Que el sensor de suelo no consuma de forma continua (puede alimentarse desde un GPIO + MOSFET para desconectarlo entre mediciones en una mejora posterior).
5) Validación de payload/CRC:
– Si alteras el código del receptor para forzar un CRC erróneo, debe reportar ERR CRC/cabecera, verificando que el emisor genera CRC correcto.
6) Verificación de duty-cycle:
– Para EU868, con WAKE_INTERVAL_S=900 (15 min), el duty-cycle es seguro. Si reduces el intervalo, asegúrate de cumplir normativa (1% por sub-banda típica).
Troubleshooting
1) No aparece el puerto serie:
– Instala el driver CP210x adecuado.
– Cambia de cable (evita “solo carga”).
– En Linux, añade usuario a dialout y reabre sesión.
– Prueba otro puerto USB (en PC de sobremesa, los traseros suelen ser más fiables).
2) Fallo al entrar en modo programación (upload):
– Mantén pulsado BOOT (GPIO0 a GND) mientras conectas USB, o pulsa RST manteniendo BOOT.
– En PlatformIO, intenta: pio run -t upload –upload-port /dev/ttyUSB0 (ajusta puerto).
– Desconecta otros dispositivos serie que “secuestren” el puerto.
3) LoRa.begin() falla:
– Verifica antena conectada y banda correcta (LORA_FREQ_MHZ).
– Confirma pines del SX1276 (para TTGO: SS=18, RST=14, DIO0=26). Si usas otro modelo, ajusta.
– No mezcles región (EU868 vs US915) con frecuencias no válidas.
4) No se observan lecturas I2C:
– Confirmar 3V3 y GND a los módulos.
– Verifica que BME280 no sea en realidad BMP280 (sin humedad).
– Comprueba direcciones: muchos BME280 vienen a 0x76; si no responde, prueba 0x77 en el código (el ejemplo ya intenta ambas).
– Cableado SDA/SCL correcto (21/22) y longitud/ruido razonables.
5) ADS1115 saturado o lecturas erróneas:
– Revisa GAIN. Con GAIN_ONE (±4.096 V) y VDD de 3.3 V es adecuado; si la señal es baja, podrías usar GAIN_TWO para mejor resolución.
– Asegura masas comunes y que el sensor de suelo se alimenta a 3.3 V (no 5 V).
6) Deep sleep no reduce consumo:
– Asegura que LoRa.sleep() se llama antes de esp_deep_sleep_start().
– Verifica que no haya periferia externa alimentada sin necesidad (sensores).
– Revisa que WiFi.mode(WIFI_OFF) y btStop() se ejecuten sin errores.
7) Mensajes esporádicos “Brownout detector”:
– Batería descargada o picos de corriente durante TX.
– Usa una LiPo en buen estado y un cable corto; considera un condensador adicional de desacoplo (p. ej. 470 µF cerca del módulo LoRa).
8) Cobertura de radio insuficiente:
– Aumenta SF (ej., LORA_SF=9 o 10) a costa de tiempo en aire.
– Mejora antena, eleva el nodo, evita obstáculos.
– Ajusta potencia respetando límites regulatorios (14 dBm EU, 20 dBm en ciertos casos con restricciones).
Mejoras/variantes
- Cortar alimentación al sensor de suelo:
- Controla VCC del sensor con un MOSFET P o un switch de alta/low-side desde un GPIO para apagarlo entre mediciones.
- Encriptación ligera:
- Añade cifrado (p. ej., AES-128 CTR) al payload antes de LoRa.write(); intercambia una clave en el receptor.
- Confirmación y reintentos:
- Implementa ACK ligero con el receptor enviando un paquete corto de vuelta; reintenta si no hay ACK, con contador limitado para no violar duty-cycle.
- LoRaWAN (si tu caso requiere):
- Sustituye LoRa “crudo” por LMIC (MCCI) y gestiona altas en un Network Server (TTN/ChirpStack). Implica más complejidad y consumo.
- ADR y autoajuste:
- Cambia SF en función del RSSI/SNR reportado por el receptor a lo largo del tiempo.
- Medición adicional:
- Usa ADS1115 canales A2/A3 para sensores de pH/EC aislados o tensiómetros, con cuidado de masas y filtrado.
- Optimización de tiempo en aire:
- Compacta más el payload (por ejemplo, delta-encoding y escala fija) y reduce la cadencia en condiciones estables.
Checklist de verificación
- [ ] He instalado PlatformIO Core 6.1.14 y Python 3.11.6; pio –version responde correctamente.
- [ ] El driver CP210x está instalado y el puerto serie aparece al conectar la TTGO LoRa32.
- [ ] He creado el proyecto con board=ttgo-lora32-v1 y he pegado platformio.ini con versiones y build_flags indicados.
- [ ] He cableado BME280 y ADS1115 a SDA=21 y SCL=22, con 3V3 y GND compartidos.
- [ ] El sensor de suelo está conectado a A0 del ADS1115; el divisor VBAT (100k/100k) a A1.
- [ ] El transmisor compila, flashea y muestra por serie la línea de mediciones y “TX OK”.
- [ ] Puedo ver el incremento de SEQ en cada despertar y cambios en SOIL al humedecer/secar la sonda.
- [ ] En validación por aire (opcional), el receptor TTGO LoRa32 decodifica tramas “AG v1” con CRC OK.
- [ ] El consumo cae notablemente en deep sleep tras unos segundos (verificado con medidor).
- [ ] He ajustado LORA_FREQ_MHZ/SF/PA según mi región y cobertura requerida.
Con este caso práctico tendrás un nodo agrícola de telemetría LoRa de bajo consumo funcionando con el modelo exacto TTGO LoRa32 (ESP32 + SX1276) y sensores BME280 + ADS1115, listo para desplegar en campo y extender con mejoras avanzadas.
Encuentra este producto y/o libros sobre este tema en Amazon
Como afiliado de Amazon, gano con las compras que cumplan los requisitos. Si compras a través de este enlace, ayudas a mantener este proyecto.



