Un predio agrícola en el secano costero, una bodega en un camino interior, una faena minera a media hora del pueblo más cercano. Los tres tienen el mismo problema: quieres saber si alguien entró, y no hay internet, no hay cobertura decente, y tirar cable hasta allá cuesta más que lo que estás cuidando.

Este proyecto resuelve exactamente eso con dos piezas: un radar mmWave RD-03D que detecta personas por ondas de radio. no por calor, no por luz. y un enlace LoRa que lleva los datos varios kilómetros sin router, sin antena celular y sin cuenta en ningún servicio.

Al terminar esta guía vas a tener un transmisor que detecta y sigue a una persona entregando posición X/Y, distancia, ángulo y velocidad, y un receptor que muestra esos datos en su pantalla OLED y los entrega al computador para una interfaz tipo radar. Y vas a saber tres cosas que el tutorial original no menciona y que en Chile te van a ahorrar un fin de semana: en qué banda tienes que trabajar acá, por qué el código tal cual satura el enlace LoRa, y cómo depurar el UART cuando el radar no entrega nada.

Bloque 1. Por qué esta combinación y no otra

El problema de comunicación

Casi todo lo que se vende como "smart" depende de WiFi o Bluetooth, y ahí está el techo:

  • Bluetooth trabaja en el rango de 10 a 30 metros.
  • WiFi cubre típicamente 50 a 100 metros, y necesita router e infraestructura.

Ninguno de los dos sirve cuando el punto a vigilar está a un kilómetro y no hay nada instalado. LoRa está construido justo para eso: transmite a varios kilómetros con consumo muy bajo. No necesita internet, no necesita router. Dos módulos LoRa se hablan directo.

El precio de esa distancia es el ancho de banda: LoRa manda pocos bytes, pocas veces. Ténlo presente, porque en el Bloque 5 se convierte en el problema principal de este proyecto.

El problema de detección

Acá entra el RD-03D, y conviene entender en qué se diferencia del sensor que probablemente ya usaste.

Sensor PIR HC SR501, el detector de movimiento tradicional

Un PIR como el HC SR501 detecta cambios de radiación infrarroja: básicamente, calor que se mueve. Funciona bien y cuesta poco, pero solo entrega un "hay algo" o "no hay nada", se confunde con corrientes de aire caliente y no ve a través de nada.

El módulo de radar mmWave RD-03D

El RD-03D trabaja con ondas de radio milimétricas. Eso le da tres propiedades que un PIR no tiene:

  1. Entrega posición, no solo presencia. Coordenadas X e Y, distancia, ángulo y velocidad del objetivo.
  2. Sigue hasta tres objetivos en movimiento al mismo tiempo, y su modo de detección es configurable.
  3. Atraviesa materiales. Cartón, madera, plástico, vidrio, cortinas. Funciona en oscuridad total y con niebla.

Prueba de seguimiento con el objetivo detrás de un obstáculo

Y detecta movimientos mínimos: una persona parada, sentada o arrastrándose sigue apareciendo. Por eso se le llama sensor de presencia humana y no sensor de movimiento.

Interfaz del RD-03D con tres objetivos detectados de forma simultánea

Junta las dos tecnologías y tienes un sistema que detecta movimiento en tiempo real, sigue posiciones con precisión, transmite a kilómetros y funciona sin internet.

Bloque 2. Antes de comprar: la banda de frecuencia

Esta sección no está en el tutorial original y es la que más problemas evita.

El código publicado define la banda así:

C++
#define BAND 866E6

866 MHz es la banda europea (EU868). En Chile la banda ISM de uso libre en ese vecindario es 915 MHz (el tramo 902 a 928 MHz, la misma región que usan Estados Unidos y Australia como AU915/US915). Si compras módulos de 868 MHz y los operas acá, dos cosas van mal a la vez:

  • Estás transmitiendo fuera de la banda de uso libre que corresponde en el país.
  • El rendimiento se cae, porque el filtro de radiofrecuencia y la antena del módulo están sintonizados a otra frecuencia. Una antena de 868 MHz en un módulo de 915 MHz desadapta la impedancia y te comes buena parte del alcance que fuiste a buscar.

Entonces, para Chile:

  1. Compra los TTGO LoRa32 en versión 915 MHz y sus antenas de 915 MHz. El módulo se ve idéntico; lo que cambia es el chip de RF y el filtro. Revisa la descripción antes de pagar.
  2. En los dos programas, transmisor y receptor, cambia la línea a:
C++
#define BAND 915E6
  1. Nunca energices un módulo LoRa sin la antena conectada. La etapa de potencia puede dañarse al transmitir contra un circuito abierto. Es la causa número uno de módulos muertos en el primer encendido.

Con esto resuelto, sigue el resto.

Bloque 3. Hardware y conexiones

La lista es corta a propósito:

  • Un par de TTGO LoRa32 (versión 915 MHz). Se recomiendan porque ya traen el módulo LoRa y una pantalla OLED SSD1306 integrada, así que no tienes que conectar nada extra en el receptor.
  • Un módulo de radar mmWave RD-03D.
  • Antenas 915 MHz para ambos módulos.
  • Cables Dupont hembra hembra para las tres conexiones del radar.
  • Alimentación: en el receptor basta un cargador de celular; en el transmisor, si va a terreno, una batería y un regulador de 5 V.

Cableado del transmisor

Son tres cables y ya:

RD-03D TTGO LoRa32
5V 5V
GND GND
TX GPIO16

Fíjate en la dirección: el pin TX del radar va al pin que el ESP32 usa como RX. Es el error más común del montaje. conectar TX con TX. y da exactamente el mismo síntoma que un radar muerto: silencio absoluto en el puerto serie.

El radar se alimenta con 5 V, pero su UART trabaja en niveles de 3,3 V, que es justo lo que el ESP32 espera. No necesitas conversor de nivel.

En el receptor no conectas nada. La pantalla ya está integrada. Si quieres, puedes agregar un buzzer para que suene al detectar movimiento, pero es opcional y no forma parte del montaje base.

El conflicto de pines que hay que conocer

Acá va un detalle que el tutorial original no advierte y que se ve leyendo los dos programas juntos.

En el receptor, el reset de la pantalla OLED está definido así:

C++
#define OLED_RST 16

Y en el transmisor, el pin de entrada del radar es:

C++
#define RX_PIN 16

Es el mismo GPIO16. En este proyecto no choca, porque cada función vive en una placa distinta: el transmisor no usa su pantalla y el receptor no tiene radar. Pero si en algún momento quieres poner radar y pantalla en la misma placa. por ejemplo para un equipo todo en uno. , tienes que mover el radar a otro pin de entrada libre y ajustar RX_PIN. Si no, la pantalla se resetea sola cada vez que llega un byte del radar.

Bloque 4. El código

Primero instala las librerías desde el gestor de librerías del IDE de Arduino: LoRa, Adafruit GFX y Adafruit SSD1306.

Transmisor

C++
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>

// Radar Pins (As requested)
#define RX_PIN 16
#define TX_PIN 17 // not connected
#define BAUD_RATE 256000

// LoRa Pins (TTGO LoRa32 Standard)
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26
#define BAND 866E6 

// Radar Data Variables
uint8_t RX_BUF[64] = {0};
uint8_t RX_count = 0;
int16_t target1_x = 0, target1_y = 0, target1_speed = 0;
float target1_distance = 0, target1_angle = 0;

// LoRa Protocol Variables
byte localAddress = 0xBB;     
byte destination = 0xFF;      
byte msgCount = 0;

uint8_t Single_Target_CMD[12] = {0xFD, 0xFC, 0xFB, 0xFA, 0x02, 0x00, 0x80, 0x00, 0x04, 0x03, 0x02, 0x01};

void processRadarData();
void sendLoRaPacket();

void setup() {
    Serial.begin(115200); // Debug
    
    // Initialize Radar
    Serial1.begin(BAUD_RATE, SERIAL_8N1, RX_PIN, TX_PIN);
    Serial1.write(Single_Target_CMD, sizeof(Single_Target_CMD));
    
    // Initialize LoRa
    SPI.begin(SCK, MISO, MOSI, SS);
    LoRa.setPins(SS, RST, DIO0);
    if (!LoRa.begin(BAND)) {
        Serial.println("LoRa Init Failed");
        while (1);
    }
    Serial.println("Transmitter Ready - Sending Radar Data via LoRa");
}

void loop() {
    // 1. Read from Radar
    while (Serial1.available()) {
        uint8_t b = Serial1.read();
        RX_BUF[RX_count++] = b;

        if (RX_count >= sizeof(RX_BUF)) RX_count = 0;

        // Check for Footer: 0x55 0xCC
        if (RX_count > 1 && RX_BUF[RX_count - 1] == 0xCC && RX_BUF[RX_count - 2] == 0x55) {
            processRadarData();
            sendLoRaPacket(); // Send packet immediately after receiving radar frame
        }
    }
}

void processRadarData() {
    if (RX_count >= 30) {
        target1_x = (RX_BUF[4] | (RX_BUF[5] << 8)) - 0x200;
        target1_y = (RX_BUF[6] | (RX_BUF[7] << 8)) - 0x8000;
        target1_speed = (int16_t)(RX_BUF[8] | (RX_BUF[9] << 8)) - 0x10;
        target1_distance = sqrt(pow(target1_x, 2) + pow(target1_y, 2)) / 10.0;
        target1_angle = atan2(target1_y, target1_x) * 180.0 / PI;
        
        RX_count = 0;
    }
}

void sendLoRaPacket() {
    // Create CSV payload
    String payload = String(target1_x) + "," + 
                     String(target1_y) + "," + 
                     String(target1_distance) + "," + 
                     String(target1_angle) + "," + 
                     String(target1_speed);

    LoRa.beginPacket();
    LoRa.write(destination);
    LoRa.write(localAddress);
    LoRa.write(msgCount++);
    LoRa.write(payload.length());
    LoRa.print(payload);
    LoRa.endPacket();
}

Lo importante de leer acá, más allá de subirlo y que funcione:

El comando de inicialización. El arreglo Single_Target_CMD es la trama que pone al radar en modo de un solo objetivo. El RD-03D soporta seguimiento de hasta tres, pero este programa pide el modo simple porque es el que cabe cómodo en un paquete LoRa. Si más adelante quieres los tres objetivos, ahí es donde cambia la cosa.

La detección de trama. El bucle lee byte a byte y busca el pie de trama 0x55 0xCC. Cuando lo encuentra, sabe que tiene una lectura completa y la procesa. Es un parser sin librería, leyendo el protocolo directo del puerto serie.

Los desplazamientos raros. En processRadarData() verás restas como - 0x200, - 0x8000 y - 0x10. No son magia: el módulo entrega las coordenadas y la velocidad con un desplazamiento fijo para poder representar valores negativos sin usar complemento a dos. Restarlo devuelve el número con signo real. Después, distancia y ángulo se calculan con Pitágoras y atan2 sobre esas coordenadas ya corregidas.

El baud rate. 256000 no es un valor estándar de UART. El ESP32 lo maneja sin problema por hardware, pero si intentas espiar el radar directamente con un adaptador USB serial barato, muchos no soportan esa velocidad y vas a ver basura. No es que el radar esté malo: es el adaptador.

Receptor

C++
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

// LoRa Pins
#define SCK 5
#define MISO 19
#define MOSI 27
#define SS 18
#define RST 14
#define DIO0 26
#define BAND 866E6 

// OLED Pins
#define OLED_SDA 4
#define OLED_SCL 15 
#define OLED_RST 16
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RST);

void setup() {
    Serial.begin(115200); // Serial to Radar Animation Software

    // Initialize OLED
    pinMode(OLED_RST, OUTPUT);
    digitalWrite(OLED_RST, LOW); delay(20); digitalWrite(OLED_RST, HIGH);
    Wire.begin(OLED_SDA, OLED_SCL);
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C, false, false);
    display.clearDisplay();
    display.setTextColor(WHITE);
    display.setCursor(0,0);
    display.println("Receiver Ready");
    display.display();

    // Initialize LoRa
    SPI.begin(SCK, MISO, MOSI, SS);
    LoRa.setPins(SS, RST, DIO0);
    if (!LoRa.begin(BAND)) {
        while (1);
    }
}

void loop() {
    int packetSize = LoRa.parsePacket();
    if (packetSize) {
        // Read header
        LoRa.read(); // destination
        LoRa.read(); // sender
        LoRa.read(); // msgId
        byte incomingLength = LoRa.read();

        String incoming = "";
        while (LoRa.available()) {
            incoming += (char)LoRa.read();
        }

        // Only parse if length matches
        if (incoming.length() == incomingLength) {
            parseAndOutput(incoming);
        }
    }
}

void parseAndOutput(String data) {
    // Find commas
    int c1 = data.indexOf(',');
    int c2 = data.indexOf(',', c1 + 1);
    int c3 = data.indexOf(',', c2 + 1);
    int c4 = data.indexOf(',', c3 + 1);

    if (c1 != -1 && c4 != -1) {
        String x = data.substring(0, c1);
        String y = data.substring(c1 + 1, c2);
        String dist = data.substring(c2 + 1, c3);
        String angle = data.substring(c3 + 1, c4);
        String speed = data.substring(c4 + 1);

        // 1. Output to Computer Software
        Serial.print(x); Serial.print(",");
        Serial.print(y); Serial.print(",");
        Serial.print(dist); Serial.print(",");
        Serial.print(angle); Serial.print(",");
        Serial.println(speed);

        // 2. Output to OLED
        display.clearDisplay();
        display.setTextSize(1);
        display.setCursor(0,0);
        display.print("X: "); display.print(x);
        display.print("  Y: "); display.println(y);
        
        display.setCursor(0, 15);
        display.setTextSize(2);
        display.print(dist); display.println(" cm");
        
        display.setTextSize(1);
        display.setCursor(0, 35);
        display.print("Angle: "); display.println(angle);
        
        display.setCursor(0, 50);
        display.print("Speed: "); 
        display.setTextSize(2);
        display.println(speed);
        
        display.display();
    }
}

El receptor hace tres cosas: recibe el paquete LoRa, lo parsea separando por comas, y saca el resultado por dos vías en paralelo. el puerto serie hacia el computador y la pantalla OLED. .

Fíjate en la validación: if (incoming.length() == incomingLength). El programa manda el largo del payload en la cabecera y solo procesa si el largo recibido coincide. Es una verificación mínima pero efectiva contra paquetes truncados, que en un enlace largo pasan.

Bloque 5. El problema que aparece en terreno: el enlace se satura

Esta parte no está en el original y es, con diferencia, lo que más te va a cambiar el resultado.

Mira dónde se llama al envío en el transmisor: sendLoRaPacket() está dentro del bucle que lee el puerto serie, justo después de cada trama completa del radar. O sea: un paquete LoRa por cada trama del radar.

El RD-03D entrega tramas a decenas de veces por segundo. LoRa, en cambio, con parámetros conservadores necesita del orden de decenas de milisegundos de aire por paquete. Las cuentas no dan: estás pidiendo mandar mucho más de lo que el enlace puede sacar.

Los síntomas en terreno son estos, y son fáciles de confundir con otra cosa:

  • El receptor muestra datos con retraso creciente respecto al movimiento real.
  • El alcance efectivo es mucho menor que el prometido, porque para sostener esa tasa terminas usando un factor de dispersión bajo, que es justamente el que menos alcanza.
  • Aparecen paquetes perdidos o truncados de forma intermitente.

La solución es limitar la tasa de envío. Mandar dos a cinco actualizaciones por segundo es más que suficiente para seguir a una persona caminando, y libera el enlace para usar parámetros de mayor alcance. El patrón es el clásico con millis():

C++
unsigned long ultimoEnvio = 0;
const unsigned long INTERVALO_MS = 250;  // 4 paquetes por segundo

if (millis() - ultimoEnvio >= INTERVALO_MS) {
    ultimoEnvio = millis();
    sendLoRaPacket();
}

Sigues leyendo el radar a toda velocidad. así no pierdes sincronía de trama. pero solo transmites la última posición conocida cuatro veces por segundo.

Bloque 6. Medir el alcance real, no el del catálogo

"Varios kilómetros" es la cifra de folleto, en línea de vista y con condiciones ideales. Lo que te sirve es el número de tu instalación, y medirlo es fácil.

La librería LoRa entrega dos indicadores por cada paquete recibido. Agrégalos en el receptor, dentro del bloque que ya procesa el paquete:

C++
Serial.print("RSSI: ");
Serial.print(LoRa.packetRssi());
Serial.print(" dBm  SNR: ");
Serial.println(LoRa.packetSnr());

Cómo interpretarlos:

  • RSSI es la potencia recibida en dBm. Más cerca de cero es mejor. Valores sobre, 100 dBm suelen ser cómodos; bajo, 120 dBm ya estás al filo.
  • SNR es la relación señal ruido en dB. LoRa puede demodular con SNR negativo, que es parte de su gracia, pero si estás bajo, 10 dB el enlace está trabajando al límite y cualquier obstáculo lo tumba.

El procedimiento de medición en terreno: deja el transmisor fijo, camina alejándote con el receptor y anota RSSI cada 100 metros. Vas a ver el punto donde la curva se cae de golpe. normalmente cuando pierdes línea de vista, no cuando se acaba la distancia. . Ese es tu alcance real, y casi siempre está más determinado por la altura de las antenas que por la potencia. Subir el transmisor tres metros suele hacer más que cualquier ajuste de software.

Bloque 7. Puesta en marcha y qué deberías ver

Interfaz de radar en vivo mostrando la posición detectada en tiempo real

Con el transmisor instalado en el punto a vigilar y el receptor alimentado con un cargador común, los datos empiezan a llegar de inmediato: posición X e Y, distancia, ángulo y velocidad, actualizándose en tiempo real.

En la pantalla OLED ves todos los valores en texto. Y ahí aparece un problema de usabilidad honesto que el autor original detectó bien: cuesta procesar cinco números en la cabeza y traducirlos a "dónde está la persona". Por eso vale la pena la interfaz tipo radar, donde ves la posición directamente en un plano.

La versión en el computador se alimenta del puerto serie del receptor: cada línea es un CSV con x, y, distancia, ángulo, velocidad, que cualquier script de Python puede leer y dibujar. Y para cuando no tienes un computador a mano. en terreno, en un cerro, en una ronda nocturna. la misma vista de radar se puede dibujar en la propia OLED, y el sistema queda completamente portátil.

Alimentación en terreno

Si el transmisor va a un lugar sin red eléctrica, el esquema que funciona es una batería de litio más un regulador que entregue 5 V estables con margen de corriente. Un regulador reductor que acepte un rango amplio de entrada (del orden de 9 a 28 V) te deja alimentar tanto desde un pack de baterías como desde un panel solar con su controlador, sin cambiar nada del montaje.

Variantes y mejoras

Tres extensiones concretas que no están en el tutorial original:

1. Pasar a los tres objetivos simultáneos. El código publicado usa el modo de un objetivo. El RD-03D soporta tres, y el cambio es reemplazar la trama de configuración Single_Target_CMD por la de múltiples objetivos y ampliar el parser para leer los tres bloques de la trama. El payload LoRa crece a unos 15 valores, así que aprovecha la limitación de tasa del Bloque 5 y manda solo los objetivos activos. Sirve para contar cuántas personas entraron, no solo si entró alguien.

2. Registro con marca de tiempo en microSD. Agrega un módulo microSD por SPI en el receptor y guarda cada detección con fecha y hora en un CSV. Con eso dejas de tener un sistema de aviso y pasas a tener un registro auditable: a qué hora hubo movimiento, en qué zona y a qué velocidad. Para un predio o una bodega, ese archivo vale más que la alarma en vivo. Ojo con el bus SPI: LoRa ya lo está usando, así que la microSD debe ir con su propio pin de selección y no puedes hablarle a las dos al mismo tiempo.

3. Repetidor LoRa para doblar una esquina. Si entre el transmisor y tu punto de monitoreo hay un cerro, un tercer TTGO LoRa32 alimentado con panel solar puede actuar como repetidor: recibe el paquete y lo retransmite. Con la cabecera que ya trae el protocolo del proyecto (destination, localAddress, msgCount) tienes lo mínimo para descartar duplicados y evitar que el repetidor se retransmita a sí mismo.

Personalización para Chile

En MechatronicStore encuentras la mayor parte de este montaje con stock local. Lo que corresponde buscar en el catálogo:

  • Módulo LoRa SX1278 / SX1276. el corazón del enlace. Verifica que sea la versión de 915 MHz, que es la banda de uso libre en Chile. La variante de 433 MHz que también se vende acá funciona en otra banda y no es intercambiable con el código de esta guía sin cambiar la frecuencia y la antena.
  • Placa ESP32 DevKit. si armas el enlace con módulo LoRa suelto en vez del TTGO integrado. Es la ruta más disponible localmente.
  • Display OLED SSD1306 0.96". la pantalla del receptor, si tu placa no la trae integrada. Va por I2C con dos cables.
  • Protoboard 830 puntos y jumpers hembra hembra 20 cm. para el prototipo antes de soldar.
  • Cable USB-C. programación y alimentación del receptor.
  • Regulador LM2596 ajustable. para bajar de una batería de litio a los 5 V que pide el radar en la instalación de terreno.
  • Batería 18650 con portapilas. alimentación portátil del transmisor.

Equivalencias respecto al artículo original:

  • Donde el original recomienda el TTGO LoRa32 (placa que integra ESP32 + LoRa + OLED), el equivalente funcional con piezas sueltas es ESP32 DevKit + módulo LoRa SX1278 915 MHz + OLED SSD1306. Es más cableado, pero se consigue mejor acá y sale parecido de precio. Los pines del código cambian: los #define de SCK, MISO, MOSI, SS, RST y DIO0 corresponden al ruteo interno del TTGO y hay que ajustarlos a como conectes tú el módulo.
  • El RD-03D es un módulo específico y no tiene reemplazo directo: es lo que hace posible el proyecto. Si no lo encuentras, no lo sustituyas por un PIR, porque un PIR no entrega posición ni distancia y el 90 % de este tutorial deja de tener sentido.
  • Donde el original menciona un pack de litio 4S de dron y una fuente propia de 5 V/3 A, en Chile lo más simple es un portapilas 18650 con un LM2596 ajustado a 5 V.

Costo aproximado del montaje completo con piezas locales: del orden de CLP $90.000 a $130.000 según qué placas uses. Es una estimación, no un precio de lista.

Recursos

Versión chilena inspirada en el proyecto de Engr. Shahzada Fahad en Electronic Clinic, adaptada a la banda de 915 MHz y a componentes en stock local en MechatronicStore.