Sentinel is an indoor positioning and geofencing system built with an ESP32, Node-RED, and Python. It simulates UWB (Ultra-Wideband) Time-of-Flight distances, calculates 2D positions via trilateration, and visualizes the hardware in a web dashboard with real-time restricted-zone alerting.
The system is broken down into four main micro-components:
- The Edge Device (ESP32): Generates (simulates) distances to 3 fixed anchors. Instead of heavy processing, it sends a highly efficient, packed binary payload (C-struct) over TCP. It also listens for
ALERTorSAFEreplies to toggle an onboard hardware LED. - The Gateway Traffic Controller (Node-RED): Unpacks the raw bytes from the ESP32 into a clean JSON object. It forwards this data to the math engine, receives the calculated (X,Y) coordinates, determines if the tag has breached the geofenced area, and updates both the ESP32 and the Web UI.
- The Math Engine (Python microservice): A Flask API running
scipy.optimize. It takes the 3 raw anchor distances and uses the Least-Squares trilateration algorithm to pinpoint the exact X,Y coordinates of the tag. - The Web Dashboard (React): A standalone frontend under
web/for the new operator dashboard. The older Node-RED dashboard flow still exists undergateway/nodered/.
sentinel/
├── README.md <-- This documentation
├── docker-compose.yml <-- Main container stack (project name: gateway)
│
├── device/ <-- ESP32 Edge Code (ESP-IDF C)
│ ├── CMakeLists.txt <-- Project build config
│ ├── sdkconfig.defaults
│ └── main/
│ ├── CMakeLists.txt <-- Component requirements (includes math, gpio, etc.)
│ └── tcp_client.c <-- Main firmware: Simulates movement, packs binary, handles LED
│
├── gateway/ <-- Server-side Services
│ ├── install.sh <-- Gateway bootstrap and Node-RED flow provisioning
│ ├── nodered/
│ │ ├── tcp_flow_exported.json <-- The full Node-RED flow (TCP, Geofencing, Dashboard)
│ │ └── ui/
│ │ └── map_template.html <-- The Vue.js Dashboard 2.0 Map component code
│ │
│ ├── logs_api/ <-- Flask API for persisted log data
│ └── trilateration/ <-- Python Math Microservice
│ ├── app.py <-- Flask API doing Scipy Trilateration
│ └── requirements.txt <-- Python dependencies (flask, scipy, numpy)
└── web/ <-- Standalone React frontend dashboard
├── package.json
├── simulation/ <-- Scripts that generate live-positioning data for testing
├── public/live/ <-- Runtime live-positioning JSON feed location
└── src/
The main runtime path is Docker Compose. The root docker-compose.yml starts Node-RED, the Python Math Engine, the logs API, PostgreSQL, and the React dashboard.
Run the gateway installer once to configure USB permissions, seed the Node-RED flow, and start the Docker stack:
cd gateway
sudo ./install.shWhat this script does:
- Sets up persistent USB permissions (
udevrules) for the ESP32 serial connection so you don't need tochmod. - Creates
gateway/dataand pre-seedspackage.jsonwith the required Node-RED plugins (@flowfuse/node-red-dashboardandnode-red-node-serialport). - Imports
gateway/nodered/serial_flow_exported.jsonas the default Node-RED flow. - Starts the main Docker Compose stack from the repository root
docker-compose.yml. - Forces Node-RED plugin installation inside the container.
After the first provisioning run, start the full system from the repository root:
docker compose up -dTo stop the stack:
docker compose downOnce the stack finishes initializing (takes ~30 seconds), Node-RED will be available at http://localhost:1880, the legacy Node-RED dashboard at http://localhost:1880/dashboard, and the React dashboard at http://localhost:5173.
Boot up the ESP-IDF environment, compile the C code, and flash it to your ESP32.
cd device
# Make sure your ESP-IDF export script is sourced first!
idf.py build flash monitorNote: Make sure to update the HOST_IP_ADDR in tcp_client.c if your Node-RED server is running on a different IP address.
The Docker stack already runs these services. Only run them natively when testing a service outside Docker.
Run the Python Math Engine natively:
cd gateway/trilateration
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt
python app.pyRun the React dashboard natively:
cd web
npm install
npm run devThe React dashboard is written against a generic live-positioning feed, not against simulation-specific code. In production, the same data shape can be written by the gateway/device pipeline. For local testing, web/simulation/entry-zone-simulation.js acts as a temporary data producer.
The dashboard polls:
web/public/live/positioning.json
That JSON contains the current anchors, tags, danger-zone polygon, and tag status. The React app does not know whether the file was produced by the simulation script or by a future device integration.
Start the web dashboard stack:
docker compose up -d webRun the entry-zone simulation inside the Docker web container:
docker compose exec -w /app web npm run simulate:entry-zoneWhile this command is running, it continuously updates public/live/positioning.json with a tag moving into the four-anchor danger zone near entries 14-15 / 16-17. Open the React dashboard at:
http://localhost:5173
When the tag enters the restricted zone, the dashboard posts the alert to the logs API, which stores it in PostgreSQL. Logs are visible at:
http://localhost:5173/logs
Stop the running simulation command with Ctrl+C. To remove the generated live feed and return the dashboard to its default fallback view:
docker compose exec -w /app web npm run simulate:clearThe same commands can be run natively from web/:
npm run simulate:entry-zone
npm run simulate:clear- ESP32 calculates a perfect mathematical circle path, turning that path into "simulated distances" from the anchors.
- ESP32 sends a 22-byte raw binary packet to Node-RED via TCP port 1234.
- Node-RED converts the bytes to JSON and sends them via HTTP POST to
http://localhost:5000/calculate. - Python replies with the exact
{"x": ..., "y": ...}. - Node-RED checks if the
X,Ycoordinates fall inside the 10x10 restricted central zone (Geofence). - Node-RED pushes the coordinates/alert status to the Vue Canvas to redraw the Map.
- Node-RED replies to the ESP32 TCP socket with either
"ALERT\n"or"SAFE\n". - ESP32 parses the reply and toggles the blue LED on GPIO 2.
The device/test directory contains implementations for an alternative communication layer using ESP-NOW to combat Wi-Fi congestion.
- esp_now_receiver:
A transparent USB-Serial bridge. It listens for 22-byte UWB payloads over the ESP-NOW protocol and blindly redirects the binary structs directly to the hardware UART (
/dev/ttyUSB0), bypassing standard strings and potential line-feed misalignments entirely. - esp_now_sender:
It replicates the exact
tcp_client.c22-byte layout but pushes it viaesp_now_sendto the hardcoded MAC address of the receiver. It implements dynamic coordinate sweeping (X/Y geofence bounds simulation) to thoroughly test the Node-RED trilateration and data validation scripts. By mocking movement across 12-meter bounds at a 10Hz transmission frequency, we map payload stability directly to the Gateway without the need for active RF anchors.
The primary flow uses TCP inside Node-RED. However, the ESP-NOW serial implementation is built to hot-plug automatically via gateway/install.sh:
- The container uses
privileged: trueand maps/dev:/dev, so any/dev/ttyUSB0device is automatically hot-plugged inside Docker when connected! - The
udevrules automatically make the port writeable (chmod 666happens natively via systemd). - The
serial_flow_exported.jsonflow is auto-imported, and includes the highly robust Sliding Window Buffer algorithm necessary to cleanly slice and extract binary UWB payloads from mixed UART streams out of the ROM Bootloader chunk.
(Note for WSL Users: Use usbipd-win like usbipd attach --wsl to pass the device from Windows to WSL, and Node-RED will pick it up instantly).