Wifi & BLE driven passenger flow metering with cheap ESP32 boards https://cyberman54.github.io/ESP32-Paxcounter/
Find a file
mon d56ea014c0 docs: fix table layout in operator instructions PDF
Replace emoji glyphs not supported by Calibri, widen table separator
columns so pandoc generates proportional LaTeX column widths (avoids
the single-line cramped output in the troubleshooting table), and add
YAML front-matter for consistent font/margin/table-spacing settings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-02 15:37:35 +02:00
.github added more boards to ci/cd workflow 2025-02-03 09:52:57 +01:00
Codeck_chirpstack fix: tag 0x90/0x91 responses with a header byte, fix stale docs 2026-06-30 14:25:17 +02:00
docs fix: tag 0x90/0x91 responses with a header byte, fix stale docs 2026-06-30 14:25:17 +02:00
herramientas_operario docs: fix table layout in operator instructions PDF 2026-07-02 15:37:35 +02:00
include feat: redesign display cycle to logo, time, summary, deveui+QR (5s each) 2026-06-30 12:13:00 +02:00
lib fix: stop leaking the BLE HCI event task/queue on every scan toggle 2026-06-30 12:55:35 +02:00
shared feat: per-page display timing and faster time-sync retry 2026-06-30 13:31:20 +02:00
src fix: stop blocking OTAA join when no NVS-provisioned keys exist 2026-06-30 18:29:15 +02:00
tools/usb_updater fix: stop blocking OTAA join when no NVS-provisioned keys exist 2026-06-30 18:29:15 +02:00
ultimo_firmware docs: add operator package with instructions, credential template and firmware 2026-07-02 15:25:10 +02:00
.clang-format do not sort includes, breaks lmic 2018-06-10 22:18:02 +02:00
.gitignore docs: add operator package with instructions, credential template and firmware 2026-07-02 15:25:10 +02:00
LICENSE LICENSE update 2018-10-21 23:09:47 +02:00
mkdocs.yml Readme changes, added logo 2022-11-22 14:33:36 +01:00
platformio_orig.ini shared/hal/cyd.h: add cyd.h header file with hardware definitions for CYD boards 2025-09-24 14:00:19 +02:00
platformio_orig_s3.ini add ttgot3s3.h 2025-09-13 18:31:41 +02:00
README.md feat: extend 0x80 get_config to report fork-specific fields 2026-06-30 14:07:05 +02:00

ESP32-Paxcounter — Fork Ciberman / Antigravity

logo

Wifi & Bluetooth driven, LoRaWAN enabled, Paxcounter and multi-sensor appliance, built on cheap ESP32 LoRa IoT boards

Tutorial (in german language): heise.de

CodeFactor PlatformIO CI


Este repositorio es un fork personalizado del proyecto original ESP32-Paxcounter, adaptado para dispositivos LILYGO T3 v1.61 con múltiples extensiones de funcionalidad orientadas a entornos de producción con LoRaWAN/ChirpStack.

Rama activa: feature/lilygo-t3v161


Ready-to-go Hardware: LILYGO® Paxcounter LoRa

Documentation (upstream): https://cyberman54.github.io/ESP32-Paxcounter

Source Code (upstream): https://github.com/cyberman54/ESP32-Paxcounter



Caso de uso

Paxcounter es un dispositivo basado en ESP32 para medir flujos de personas y datos multi-sensor en tiempo real. Detecta señales WiFi y Bluetooth en el aire, evaluando direcciones MAC de dispositivos móviles, sin almacenar ni fingerprintear de forma persistente dichas MACs.

Los datos pueden almacenarse en tarjeta SD local, transmitirse a la nube mediante LoRaWAN (TheThingsNetwork, Helium, ChirpStack) o MQTT, o enviarse por interfaz serie.


Funcionalidades añadidas en este fork

Resumen de cambios por commit

Fecha Commit Descripción
2026-01-10 467ff7e6 SD card: almacenamiento cíclico de datos en fichero rotativo
2026-01-10 0f3a5190 Actualización libpax con mejoras en scan BLE/WiFi
2026-01-15 2dfb477c Ajustes en aggregator (conteo y acumulación)
2026-01-30 70054442 Dispositivos fijos, backfill histórico, nuevos comandos downlink
2026-02-02 4f608eca Página dedicada DevEUI en display (9 páginas totales)
2026-02-10 5ceaa6aa Backfill por tiempo (minutos hacia atrás)
2026-02-11 2623a9ef Secuencia BLE on/off para optimizar escaneo WiFi
2026-02-11 fe6e3e6d Fix libpax: de submódulo roto a directorio regular
2026-02-11 735344a3 Configuración remota de intervalos RSSI (D1/D2) por downlink
2026-02-12 140d78b4 Carga devui externa por NVS; configuración remota de records_back

1. Almacenamiento en SD card con fichero cíclico

Commit: 467ff7e62026-01-10

Los datos de conteo se graban en tarjeta SD en un fichero de tamaño máximo configurable. Cuando el fichero alcanza el límite, se rota automáticamente (escritura cíclica), garantizando almacenamiento continuo sin saturar la tarjeta.

Ficheros afectados: include/sdcard.h, src/sdcard.cpp, src/senddata.cpp


2. Clasificación de dispositivos fijos (Fixed Devices)

Commit: 700544422026-01-30

Sistema automático de clasificación que distingue dispositivos fijos (infraestructura: APs, routers, balizas BLE) de dispositivos móviles (personas). Los dispositivos fijos se excluyen del conteo de Pax.

  • Hasta 512 dispositivos WiFi fijos y 256 BLE fijos en memoria
  • Clasificación basada en: mínimo 3 apariciones consecutivas y varianza RSSI ≤ 10 dBm
  • Persistencia en tarjeta SD (load_fixed_devices_from_sd / save_fixed_devices_to_sd)
  • Comandos de gestión remota por downlink (ver sección Comandos Downlink)

Ficheros afectados: include/fixed_devices.h, src/fixed_devices.cpp, src/aggregator.cpp


3. Backfill de datos históricos

Commits: 70054442 (por registros), 5ceaa6aa (por tiempo) — 2026-01-30 / 2026-02-10

Permite solicitar por downlink el reenvío de registros históricos almacenados en el buffer del dispositivo. Útil para recuperar datos perdidos por conectividad intermitente.

  • Modo BURST: todos los registros se encolan y transmiten lo antes posible (limitado por duty cycle LoRaWAN)
  • Por número de registros (comando 0x94): indica cuántos registros pasados enviar
  • Por tiempo (comando 0x95): indica cuántos minutos hacia atrás recuperar (recomendado)
  • El payload histórico es idéntico al actual (46 bytes con timestamp), identificable por el timestamp

Ficheros afectados: src/senddata.cpp, src/rcommand.cpp, docs/downlink_commands.md


4. Doble envío histórico automático (records_back)

Commits: 140d78b4, cambios sin commit — 2026-02-12

Tras cada envío de datos en tiempo real, el dispositivo puede reenviar automáticamente los últimos N registros del buffer. El parámetro records_back controla cuántos registros se reenvían (0 = desactivado).

  • Valor por defecto: 288 registros (equivale a 24 horas con intervalos de 5 minutos)
  • Configurable de forma remota por downlink (comandos 0x9A / 0x9B)
  • Se persiste en NVRAM

Ficheros afectados: include/globals.h, src/configmanager.cpp, src/senddata.cpp


5. Clasificación por distancia RSSI (D1/D2/D3)

Commits: a973e9b6, 735344a32026-01-10 / 2026-02-11

El conteo de MACs se segrega en tres bandas de distancia según la intensidad de señal:

Banda Criterio Descripción
D1 RSSI ≥ d1 Muy cerca (ej. < 5 m)
D2 d2 ≤ RSSI < d1 Distancia media (ej. 515 m)
D3 RSSI < d2 Lejos (ej. > 15 m)

Los umbrales d1 y d2 son configurables de forma remota para WiFi y BLE por separado (comandos 0x96, 0x97, 0x98). Se persisten en NVRAM.

Ficheros afectados: include/globals.h, src/aggregator.cpp, src/rcommand.cpp


6. Optimización del escaneo BLE/WiFi

Commit: 2623a9ef2026-02-11

Implementa una secuencia de encendido y apagado del módulo BLE sincronizada con el ciclo de escaneo WiFi, para reducir interferencias y mejorar la calidad de las mediciones WiFi mientras BLE está activo.

Ficheros afectados: src/main.cpp, src/rcommand.cpp, include/globals.h


7. Página DevEUI en display

Commit: 4f608eca2026-02-02

Se añade una página dedicada en el display OLED que muestra el DevEUI del dispositivo, facilitando la identificación en campo. El total de páginas pasa de 8 a 9, con mayor duración asignada a la página DevEUI.

Ficheros afectados: src/display.cpp, include/display.h


8. Aprovisionamiento de claves LoRaWAN por NVS y puerto serie

Commit: 140d78b4 (parcial) + cambios sin commit — 2026-02-12

Las claves OTAA (DevEUI, JoinEUI, AppKey) pueden almacenarse en la memoria no volátil (NVS) del ESP32 y cargarse dinámicamente en tiempo de ejecución, sin necesidad de recompilar el firmware.

Aprovisionamiento por puerto serie (nuevo)

Al arrancar, el dispositivo espera 8 segundos por el puerto serie un comando de provisión con el formato:

PXCSET DEVEUI=<16hex> JOINEUI=<16hex> APPKEY=<32hex>

Ejemplo:

PXCSET DEVEUI=70B3D57ED0000000 JOINEUI=0000000000000001 APPKEY=A1B2C3D4E5F6A7B8C9D0E1F2A3B4C5D6

El dispositivo responde PXCOK SAVED y reinicia automáticamente con las nuevas claves. En caso de error en el formato hex: PXCERR BADHEX.

Si no hay claves válidas en NVS, el join LoRaWAN queda bloqueado (ESP_FAIL) como medida de seguridad.

Ficheros afectados: src/main.cpp, src/lorawan.cpp, lib/lorawan_keys/


9. Watchdog de escaneo BLE con recuperación escalada

Commit: c9fca1922026-06-30

El stack BLE del ESP32 puede colgarse tras varias horas de ciclos repetidos de encendido/apagado (ver punto 6). Un nuevo task monitor (ble_watchdog_task) vigila un heartbeat actualizado por el scan_scheduler_task en cada fase WiFi/BLE.

  • Nivel 1 — Reinicio suave: si el heartbeat se queda obsoleto, mata el task de escaneo, resetea el controlador BT (btStop()/btStart()), reinicializa libpax y recrea el task. La sesión LoRaWAN no se pierde.
  • Nivel 2 — Reinicio completo: si el reinicio suave no recupera el heartbeat en un ciclo completo, se ejecuta un warm restart (do_reset(true)), que conserva la sesión LoRaWAN guardada en RTC.
  • Estadísticas: cada evento se registra en un buffer circular de 30 días en NVS y en un log rotativo /wd.csv en la SD. Consultables por downlink (0x9C, ver tabla de comandos).

Ficheros afectados: include/ble_watchdog.h, src/ble_watchdog.cpp, src/main.cpp, include/globals.h, src/rcommand.cpp


10. Reinicio remoto del dispositivo

Commit: 833ecbd7 (documentación) — 2026-06-30

El comando set_reset (opcode 0x09) ya existía en el código pero no estaba documentado. Permite reiniciar el dispositivo en remoto por downlink, sin necesidad de acceso físico. El modo 0x04 (warm restart) es el recomendado para un reinicio simple: conserva la sesión LoRaWAN, evitando el coste de un rejoin.

Downlink: 09 04   (reinicio simple, conserva sesión LoRa)
Downlink: 09 00   (reinicio frío, fuerza rejoin)

Ver tabla completa de modos en docs/downlink_commands.md.


11. Actualización de firmware (OTA) desde tarjeta SD

Sin commit aúnen curso

Permite actualizar el firmware sin conexión LoRaWAN ni WiFi: basta con copiar el binario compilado a la tarjeta SD con un nombre determinado. Al arrancar, el dispositivo detecta el fichero, lo flashea y reinicia automáticamente.

Ficheros en la raíz de la SD:

Fichero Función
/paxfw.bin Imagen de firmware compilada (firmware.bin de PlatformIO)
/paxkeys.txt Opcional — claves LoRaWAN a aprovisionar antes de flashear (mismo formato que el comando serie PXCSET)

Formato de /paxkeys.txt:

DEVEUI=<16hex> JOINEUI=<16hex> APPKEY=<32hex>

Flujo de arranque:

  1. Si existe /paxfw.bin: se aprovisionan primero las claves de /paxkeys.txt (si está presente) en NVS — así un dispositivo nuevo, programado solo a través de la SD, se une a LoRaWAN con su identidad correcta desde el primer arranque del nuevo firmware.
  2. Se flashea /paxfw.bin con la librería Update de Arduino ESP32.
  3. Si tiene éxito: el fichero se renombra a .done (evita reflasheo en bucle) y el dispositivo reinicia con el nuevo firmware.
  4. Si falla: se renombra a .failed, se registra el error y el arranque continúa con el firmware actual.

Las claves LoRaWAN ya provisionadas en NVS (ver punto 8) no se pierden al actualizar firmware, ya que se almacenan independientemente del binario flasheado.

Ficheros afectados: include/ota_sd.h, src/ota_sd.cpp, src/main.cpp, lib/lorawan_keys/ (parser de claves compartido con el aprovisionamiento serie)


Comandos Downlink

El dispositivo escucha comandos remotos en el puerto LoRaWAN 2 (FPort 2). Se pueden concatenar varios comandos en un mismo downlink (máx. ~10 bytes). Metodología en dos pasos (más segura para pruebas): los cambios de configuración se aplican de inmediato pero no se persisten — si reinicias sin guardar, vuelve al último valor guardado. Para hacerlos permanentes, envía 0x21 (set_saveconfig) después.

Documentación detallada del rango extendido (fixed devices, backfill, RSSI bins): docs/downlink_commands.md. Documentación original del proyecto base: docs/remotecontrol.md.

Comandos estándar (proyecto base)

Opcode Función Parámetros Persiste solo
0x01 Límite RSSI de escaneo 1 byte: 0=desactivado, 1-255=radio con 0x21
0x02 Modo de conteo 1 byte: 0=cíclico, 1=acumulativo, 2=cíclico confirmado con 0x21
0x03 GPS on/off 1 byte: 0/1 con 0x21
0x04 Display on/off 1 byte: 0/1 con 0x21
0x05 LoRa datarate 1 byte: 0-15 con 0x21
0x06 LoRa TX power 1 byte: dBm con 0x21
0x07 LoRa ADR on/off 1 byte: 0/1 con 0x21
0x08 No-op
0x09 Reset/restart 1 byte: 0=cold, 2=factory, 3=flush queue, 4=warm (recomendado), 8=maintenance, 9=OTA WiFi
0x0A Ciclo de envío LoRa 1 byte: segundos/2, mín. 5 (10s) con 0x21
0x0B Intervalo salto canal WiFi 1 byte: segundos/100 con 0x21
0x0C Duración escaneo BLE 1 byte: segundos/100 con 0x21
0x0D Ventana sync wakeup 2 bytes BE: segundos con 0x21
0x0E Escáner BLE on/off 1 byte: 0/1 con 0x21
0x0F Antena WiFi (LoPy/FiPy) 1 byte: 0=interna, 1=externa con 0x21
0x10 Luminosidad RGB LED 1 byte: 0-100% con 0x21
0x11 Mapa de canales WiFi 2 bytes BE: bitmask 0-8191 con 0x21
0x13 Modo sensor usuario 2 bytes: nº sensor (1-3), modo (0/1) con 0x21
0x14 Máscara de payload 1 byte: bitmask (ver docs/remotecontrol.md) con 0x21
0x15 Datos BME on/off 1 byte: 0/1 con 0x21
0x16 Datos batería on/off 1 byte: 0/1 con 0x21
0x17 Escáner WiFi on/off 1 byte: 0/1 con 0x21
0x18 Reservado (no-op)
0x19 Ciclo de sueño 2 bytes BE: segundos/10, 0=sin sueño con 0x21
0x20 Cargar config desde NVRAM
0x21 Guardar config en NVRAM
0x80 Get config (puerto 3) 28 bytes: incluye ble_percent, records_back, RSSI bins (ver docs/downlink_commands.md)
0x81 Get status (puerto 2)
0x83 Get batería (puerto 8)
0x84 Get GPS (puerto 4)
0x85 Get BME (puerto 7)
0x86 Get hora/fecha (puerto 2)
0x87 Sincronizar hora
0x88 Set hora/fecha 4 bytes BE: epoch UTC

Comandos propios del fork

Opcode Función Parámetros Descripción
0x1D ble_autotune_now Ajusta blescantime automáticamente según wifichanmap/wifichancycle actuales (no persiste, mandar 0x21 después si se quiere fijo)
0x1E set_ble_percent 1 byte: 0-100 % de tiempo BLE dentro del ciclo de escaneo (resto es WiFi)
0x90 get_fixed_wifi offset (2 bytes BE) Lista MACs WiFi fijas (paginado)
0x91 get_fixed_ble offset (2 bytes BE) Lista MACs BLE fijas (paginado)
0x92 get_agg_info Info del buffer histórico
0x93 clear_fixed_devices 1 byte: 0=WiFi, 1=BLE, 2=Ambos Borra dispositivos fijos
0x94 request_backfill num_records (2 bytes BE) Backfill por número de registros
0x95 request_backfill_by_time minutos (2 bytes BE) Backfill por tiempo (recomendado)
0x96 set_wifi_rssi_bins d1_abs, d2_abs (2 bytes) Configura umbrales RSSI WiFi (D1/D2)
0x97 set_ble_rssi_bins d1_abs, d2_abs (2 bytes) Configura umbrales RSSI BLE (D1/D2)
0x98 get_rssi_bins Consulta umbrales RSSI actuales
0x99 set_flush Vacía el buffer de envío
0x9A set_records_back valor (2 bytes BE) Configura registros históricos doble envío
0x9B get_records_back Consulta valor actual de records_back
0x9C get_watchdog_stats días (1 byte) Reinicios BLE/full de los últimos N días

Ejemplos de uso (ChirpStack, FPort 2)

# Ciclo escaneo 30s con 50% BLE / 50% WiFi (sendcycle=15 -> 15x2=30s):
# Activo de inmediato, pero temporal -> añade "21" al final si quieres que sobreviva a un reinicio:
Hex: 0A 0F 1E 32       (solo para esta sesión, vuelve atrás al reiniciar)
Hex: 0A 0F 1E 32 21    (igual, pero además lo persiste en NVRAM)

# Backfill últimas 2 horas (120 min = 0x0078):
Hex: 95 00 78  |  Base64: lQB4

# Configurar RSSI WiFi D1=-70dBm, D2=-87dBm:
Hex: 96 46 57  (70=0x46, 87=0x57)

# Consultar umbrales RSSI:
Hex: 98

# Configurar records_back = 288 (24h @ 5min):
Hex: 9A 01 20  (288 = 0x0120)

# Consultar records_back:
Hex: 9B

# Reinicio remoto simple, conserva sesión LoRa:
Hex: 09 04

Licencia

Copyright 2018-2022 Oliver Brandmueller ob@sysadm.in

Copyright 2018-2022 Klaus Wilting verkehrsrot@arcor.de

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

NOTICE: Parts of the source files in this repository are made available under different licenses, see file LICENSE.txt in this repository. Refer to each individual source file for more details.


Créditos (upstream)

Thanks to

  • Oliver Brandmüller for idea and initial setup of this project
  • Charles Hallard for major code contributions to this project
  • robbi5 for the payload converter
  • Caspar Armster for the The Things Stack V3 payload converter
  • terrillmoore for maintaining the LMIC for arduino LoRaWAN stack
  • sbamueller for writing the tutorial in Make Magazine
  • Stefan for paxcounter opensensebox integration
  • August Quint for adding SD card data logger and SDS011 support
  • t-huyeng for adding a CI workflow and rework documentation
  • TD-er for bugfixings and T-Beam documentation

Créditos fork Ciberman/Antigravity

Extensiones desarrolladas por:

  • Mon (mon@phebus.es) — arquitectura, integración NVS/LoRaWAN, RSSI bins, SD cíclica, BLE/WiFi optimization
  • nicolasPascual (nicolas@techfriendly.es) — backfill histórico, fixed devices, display DevEUI, documentación downlink