Two-node river level monitoring system using Heltec WiFi LoRa 32 V4 modules and a JSN-SR04T waterproof ultrasonic sensor. The sensor node runs on battery/solar and transmits readings over LoRa to a gateway node connected to a PC via USB serial.
[JSN-SR04T]
|
[Sensor Node - Heltec V4 #1] --LoRa 915MHz--> [Gateway Node - Heltec V4 #2] --USB--> [PC]
Battery + Solar USB powered logger.py
Deep sleep between TX OLED display
| Qty | Part |
|---|---|
| 2 | Heltec WiFi LoRa 32 V4 |
| 1 | JSN-SR04T waterproof ultrasonic sensor |
| 1 | NPN transistor (2N2222, BC547, or S8050) |
| 1 | 1kΩ resistor |
| 1 | 3000mAh LiPo battery |
| 1 | Solar charge controller (TP4056 or similar) |
| 1 | Small solar panel (2–5W) |
The Heltec V4 has the SX1262 LoRa radio and OLED built in. The only external wiring is the JSN-SR04T and its power transistor.
JSN-SR04T NPN Transistor Heltec V4
┌─────────┐ ┌─────────────┐ ┌──────────────────┐
│ │ │ Collector │──────────│ 3V3 │
│ VCC ──┼────────────────┤ │ │ │
│ │ (switched) │ │ │ GPIO45 (PWR)─┐ │
│ GND ──┼────────────────┤ Emitter ───┼──────────│ GND │ │
│ │ │ │ │ │ │
│ TRIG ──┼────────────────┼─────────────┼──────────│ GPIO47 │ │
│ │ │ Base ──────┼──[1kΩ]───│ │ │
│ ECHO ──┼────────────────┼─────────────┼──────────│ GPIO48 │ │
└─────────┘ └─────────────┘ │ │ │
└──────────────┘ │
GPIO45 HIGH = sensor powered ON │
GPIO45 LOW = sensor off (saves battery) │
Power transistor wiring detail:
Heltec GPIO45 ──[1kΩ]──► Base (B)
3V3 ──────────► Collector (C) ──► JSN-SR04T VCC
GND ──────────► Emitter (E)
Battery / solar:
[Solar Panel] → [Charge Controller / TP4056] → [LiPo] → [Heltec V4 BAT pin]
The Heltec V4 has an onboard voltage divider on GPIO1 for battery monitoring — no external resistors needed. Verify the divider ratio against your board schematic; the firmware default is 2.0 (adjust BATT_ADC_RATIO in shared/packet.h if needed).
No external wiring — connect via USB-C to your PC.
Bridge / post
│
│ ← SENSOR_HEIGHT_CM (default 300 cm, set in shared/packet.h)
│
[JSN-SR04T] ← mounted face-down
│
↕ distance measured by sensor
│
~~~ water surface ~~~
water_level_mm = (SENSOR_HEIGHT_CM × 10) − distance_mm
Adjust SENSOR_HEIGHT_CM in shared/packet.h to match your installation.
river_level/
├── shared/
│ └── packet.h # Packet struct, CRC-8, all pin and config constants
├── sensor_node/
│ ├── platformio.ini
│ └── src/main.cpp # Measure → transmit → deep sleep
├── gateway_node/
│ ├── platformio.ini
│ └── src/main.cpp # Continuous RX, OLED display, JSON serial output
├── tools/
│ └── logger.py # PC-side serial reader and CSV logger
└── data/
└── readings.csv # Generated at runtime (gitignored)
Install the PlatformIO CLI:
pip install platformioEdit source files with Sublime Text as normal — PlatformIO only needs the CLI, no IDE required.
cd sensor_node
pio run -t uploadcd gateway_node
pio run -t uploadcd gateway_node
pio device monitorYou should see JSON lines like:
{"ts":1709123456,"node":1,"seq":42,"level_mm":1234,"dist_mm":456,"batt_mv":3850,"rssi":-87,"snr":7.5,"flags":0}pip install pyserialpython tools/logger.py --port COM3
# or on Linux/Mac:
python tools/logger.py --port /dev/ttyUSB0Output example:
----------------------------------------------------------------------
Time (UTC) Level (cm) Dist (mm) Batt (mV) RSSI SNR Seq
----------------------------------------------------------------------
2024-02-28 14:32:01 123.4 766 3850 -87.0 7.5 42
Rows are appended to data/readings.csv automatically.
All key parameters are in shared/packet.h:
| Constant | Default | Description |
|---|---|---|
SENSOR_HEIGHT_CM |
300 | Distance from sensor face to channel bottom (cm) |
ALARM_LEVEL_MM |
2000 | Flood alert threshold (mm) |
SLEEP_INTERVAL_S |
300 | Deep sleep duration between readings (s) |
LORA_FREQ_MHZ |
915.0 | LoRa frequency — change to 868.0 for EU |
LORA_SF |
9 | Spreading factor (7–12, higher = more range, slower) |
BATT_ADC_RATIO |
2.0 | ADC divider ratio — verify against your board schematic |
16-byte binary packet transmitted over LoRa:
| Bytes | Type | Field |
|---|---|---|
| 0 | uint8 | node_id |
| 1–2 | uint16 | sequence number |
| 3–4 | int16 | water_level_mm |
| 5–6 | uint16 | raw_distance_mm |
| 7–8 | uint16 | battery_mv |
| 9 | uint8 | temperature_c (0xFF = not available) |
| 10 | uint8 | flags (0x01=alarm, 0x02=sensor_err, 0x04=low_batt) |
| 11–14 | uint32 | uptime_s |
| 15 | uint8 | CRC-8 |
| State | Current | Duration | Energy/cycle |
|---|---|---|---|
| Deep sleep | ~10 µA | 5 min | ~50 µAh |
| Wake + measure | ~80 mA | 500 ms | ~11 µAh |
| LoRa TX (SF9) | ~120 mA | ~200 ms | ~7 µAh |
| Total/cycle | ~68 µAh |
288 cycles/day × 68 µAh ≈ ~20 mAh/day 3000 mAh LiPo → ~150 days without solar; a small 2W panel makes it indefinitely sustainable.
- Bench test sensor: USB power, disable deep sleep, aim JSN-SR04T at a tape measure — verify distance readings on serial monitor
- Bench test link: Place both nodes 1 m apart, confirm JSON appears in gateway serial output
- Range test: Walk sensor node away, check RSSI vs. distance
- Deep sleep test: Enable sleep, confirm 5-minute wake cycle
- Logger test: Run
logger.py, confirm CSV rows written correctly - Field test: Mount over a bucket at known depth, verify
level_mmmatches measurement