-
LoRaLAN WiFi Gateway: a second way into Node-RED
11/04/2026 at 15:47 • 0 comments![]()
LoRaLAN has a new gateway. The LoRaLAN WiFi Gateway does the same job as the Lora to Ethernet Bridge, but over Wi-Fi: it listens for LoRaLAN sensor packets on 915 MHz, ACKs each one, and publishes it to an MQTT broker in the same JSON format the bridge sends Node-RED (with a
gwfield added). Same radio settings, same RadioHead-style packets, so existing nodes work with it unchanged. The network overview is in the LoRaLAN repo.What it's for If you have a Wi-Fi network and LoRaLAN sensor nodes, use it as your main gateway with no Ethernet run, or put it closer to far-off sensors to extend their reach. Your Node-RED server and MQTT broker must be reachable on the Wi-Fi network it joins. You can run several gateways at once, mixing Ethernet bridges and Wi-Fi gateways, to greatly expand range. Each gets its own address, and every message says which gateway heard it. Over a VPN, a Wi-Fi gateway at a remote site can publish to your broker at home, so you can monitor remote locations across the Internet.
The build A Seeed XIAO ESP32-S3 with a Wio-SX1262 on the board-to-board connector, running MicroPython v1.29.0 and powered by USB-C. A 2.4 GHz FPC antenna sits inside the front wall, and a 915 MHz whip mounts on an SMA bulkhead in the lid. The two-piece 3D-printed enclosure (about 47 × 32 × 30 mm, no supports) holds the board by pressing its pin headers into holes in the floor, with "LoRaLAN WiFi GW" engraved on the lid. A pin test coupon in the repo helps you dial in the hole fit first.
![]()
How it's configured Wi-Fi, the broker, the gateway address and the topic prefix all go in one
secrets.py. It publishes each packet to<prefix>/gateway<address>/rx, a retained status message every minute, and online/offline availability. If Wi-Fi or MQTT drops, it keeps ACKing and queues packets. To point a node at it, setSERVER_ADDRESSin the node'sconfig.pyto the gateway's address.![]()
Files Firmware, example settings, the MQTT format, and the enclosure STL, STEP and OpenSCAD source are all in the LoRaLAN WiFi Gateway repo. Earlier logs are on the LoRaLAN project page.
Next Putting it to work and pointing nodes at it. I'm always looking for people to collaborate with. Want to try LoRaLAN on a farm, a large property or a remote site? I'd love to help get it set up.
-
Leak sensors deployed on LoRaLAN, all the way to Home Assistant
10/29/2026 at 20:11 • 0 comments![]()
The water leak sensors from the last log are now installed around the house, with several live and reporting into Home Assistant. They're the newest node type on LoRaLAN: like the gate sensor, temperature probe and weather station, they send RadioHead-style packets on 915 MHz to the Lora to Ethernet Bridge (address 2), which ACKs every packet and forwards it over Ethernet to Node-RED. Nothing changed on the bridge to add them. How all the nodes fit together is in the LoRaLAN repo, and the bridge build is written up on the blog in Local LoRa, no cloud required.
The build Every node is a Seeed XIAO ESP32-S3 with a Wio-SX1262 on the PCB sponsored by PCBWay. The 0.66 mm boards slide into card guides in a three-piece 3D-printed PLA enclosure (base, body and lid, no supports needed), and the battery divider is two 1 MΩ resistors with a capacitor on the tap. 1 MΩ keeps the divider's constant draw low. An 1100 mAh LiPo powers it, with its leads soldered to the two battery pads on the underside of the XIAO and a mini slide switch recessed into the side of the case. The XIAO charges the cell, so charging is just plugging the sensor into USB-C. All of the nodes run MicroPython v1.29.0. Thanks again to PCBWay for the boards. They came back fast and clean, and they fit the enclosure perfectly.
![]()
Parts list
- 1 × Seeed Studio XIAO ESP32-S3: MicroPython v1.29.0 (ESP32_GENERIC_S3, SPIRAM_OCT build)
- 1 × Seeed Wio-SX1262 for XIAO: On the XIAO's board-to-board connector (sold together as the XIAO ESP32-S3 + Wio-SX1262 kit)
- 1 × Carrier PCB, 0.66 mm: Made by PCBWay. Holds the XIAO, the battery divider and the probe/battery pads
- 2 × 1 MΩ resistor, 0603 (R1, R2): Battery divider, BAT+ to GND, tap on D2 (GPIO3)
- 1 × 0.1 µF capacitor, SMD (C2): From the divider tap to GND
- 1 × 1100 mAh LiPo, 50 × 29 × 7 mm: Leads soldered to the two battery pads on the underside of the XIAO. Charges by plugging the sensor into USB-C
- 1 × SS12F15 mini slide switch (Amazon): Inline in the battery's negative lead (middle pin + one outer pin)
- 2 × 2.2 × 6 mm self-tapping screw: Holds the switch ears to the enclosure
- 2 × M2.5 × 8 mm socket-head cap screw: The probe bolts, through the base
- 4 × M2.5 hex nut: Two per bolt: one captive in the base, one jam nut
- 2 × Ring lug, M2.5: Probe wire to each bolt
- 28 AWG silicone hookup wire: Probe leads and battery leads
- 1 × Slinkdsco 915 MHz long range LoRa whip antenna (red, sold for Meshtastic, Amazon): Mounted on an SMA bulkhead in the lid
- 1 × U.FL (IPEX) to SMA bulkhead pigtail: The Wio-SX1262 has a U.FL connector
- 1 × 3D-printed enclosure, PLA: Three pieces: base (rev6c), body (rev6d), lid (rev6e). Prints without supports. Print settings on request
How it's configured Every node runs the same firmware. The only per-node file is
config.py, and only the address and MQTT topics change between nodes (the repo default is address 10 withwater_sensortopics). The radio settings are shared across LoRaLAN: 915 MHz, BW125, SF7, CR4/5, with RadioHead-style addressing to the bridge at address 2 and an ACK on every packet.Sensing uses a fixed dry reference. A dry probe pins the ADC at 65,535, a reading of 64,535 or lower counts as wet, and it only returns to dry after two readings in a row at 65,200 or higher. When dry, it checks every 60 seconds, looks again after 15 seconds if a reading is close to the line, and checks every 15 seconds while wet. A heartbeat with the battery goes out every 6 hours, and a cold boot reports the real wet/dry state right away.
Home Assistant Node-RED maps each node to a name, publishes to MQTT, and sends Home Assistant MQTT discovery. Each sensor shows up as a device with a binary leak sensor, battery % and mV, and RSSI/SNR. A leak sends a notification to both of our phones, and the dashboard has a tile per sensor plus a leak badge that only shows when something is wet. Every node passed a real wet test after install, all the way to the phone alert.
![]()
![]()
The water view in Home Assistant, and a sensor in the wet state during a test.
Battery At the 6-hour heartbeat, the 1100 mAh cell runs for months on a charge, and every heartbeat reports the voltage so Home Assistant shows when one needs charging.
Files Everything is shared in the Local LoRa Moisture Sensor repo: the MicroPython firmware, a generic
config.py, the enclosure STL and STEP files plus the OpenSCAD source, the parts list, and photos. The bridge is in Lora to Ethernet Bridge, the network overview is in LoRaLAN, and earlier logs are on the LoRaLAN project page.Next A LoRaLAN repeater to extend range inside larger buildings, an analysis of antennas and placement with a real range test, and maybe a LoRaLAN garage door controller and monitor. Let's get crazy! I'm always looking for people to collaborate with and share ideas. Right now it's just me and my robot army in my shop and office thinking things up. Want to try LoRaLAN on your farm or a large property? I'd love to help get it set up for someone to test.
-
Adding a battery-powered water leak sensor to LoRaLAN
10/22/2026 at 00:27 • 0 commentsThe next node on LoRaLAN is a water leak sensor. It's built to sit on the floor next to a water heater, washer, or anywhere else a slow leak can go unnoticed, and to run for months on a battery without ever touching Wi-Fi.
**The hardware**
Each sensor is a Seeed XIAO ESP32-S3 with a Wio-SX1262 LoRa radio on its board-to-board connector, running MicroPython. It's powered by an 1100 mAh LiPo. The probe is about as simple as it gets: two bolts through the bottom of the enclosure. One bolt goes to a GPIO with the internal pull-up and the other goes to ground. Water bridging the bolts pulls the reading down. A resistor divider on another ADC pin reports the battery voltage.The PCB that ties it all together was sponsored by [PCBWay](https://www.pcbway.com/). It's a thin 0.66 mm board holding the XIAO, the radio, and the battery divider, and the boards came back fast and clean.
**How it behaves**
The sensor spends almost all its time in deep sleep. Every 5 minutes it wakes, reads the probe, and goes straight back to sleep unless something changed. When it detects water, it sends a "triggered" packet to the bridge and waits for an ACK. Then it checks every 15 seconds until things dry out and sends "dry." Every 6 hours it sends a heartbeat with the battery voltage, so a quiet sensor isn't mistaken for a dead one. The radio only powers up when there's something to send.On first boot it learns its dry baseline and stores it in RTC memory, so every sensor calibrates itself to its own probe.
**Tuning**
Bench testing gave a clear picture of the readings:
- Dry: 65,535 (fully pinned high)
- Damp rag: about 6,700 below dry
- Standing water: about 13,000 to 16,000 below dryThe wet threshold is set at 2,000 below the baseline. That catches a damp surface while dry readings stay pinned, so there are no false alarms.
**Battery**
The node runs a 6-hour heartbeat in production, and most of its life is spent in deep sleep with a floor of a few hundred microamps. That works out to roughly 3 months or more on the 1100 mAh cell. Battery voltage is logged with every heartbeat, so a measured runtime will follow in a later log.**What's next**
Nine of these are planned around the house, each with its own location in the config. A revised enclosure is in progress with an external slide switch for the battery, and the end-to-end path is already working: sensor to LoRa to the bridge, then into Node-RED, MQTT, and Home Assistant.Firmware and README: Local LoRa Moisture Sensor
-
A small buffer bug found in testing new sensor
10/11/2026 at 16:18 • 0 commentsI didn't find this bug by looking for it. I found it while bench-testing a new node: a XIAO ESP32-S3 moisture sensor with a Wio-SX1262 radio that reports its battery voltage in every heartbeat. I wanted to watch 'bat_mv' climb while the LiPo charged, so I kept resetting the sensor and watching Node-RED for each heartbeat.
They took minutes to show up, and then several arrived at once.
Following the packet
The path is sensor → LoRa Pico (RFM95, 915 MHz) → UART at 230400 baud with A5 5A framing → Wiznet Pico → websocket → Node-RED → MQTT. With both the LoRa Pico and the Wiznet board plugged into the same machine, I logged their serial output side by side.
The LoRa Pico was fine. It received each packet, sent the ACK, and handed the frame to the Wiznet within the same second. One packet left the LoRa Pico at 5:17:01 and reached Node-RED at 5:19:17. Two heartbeats from separate boots, 107 seconds apart, arrived at the same instant, and one burst had packet IDs 244, 245, 246 and 245 again. Obviously, that wasn't correct.
First theory: packet size
The sensor's early packets were chatty JSON, about 340 bytes at the bridge once base64'd and wrapped. The bridge has historically struggled with big frames, so I trimmed the payload down to the essentials (at most 95 bytes over the radio, about 210 at the bridge) and moved the location and topic mapping into Node-RED, keyed by node number (filtered in NodeRed by header_from = #)
It didn't help. The smaller frames got stuck too.
The numbers that gave it away
The Wiznet's status heartbeat carries counters, and over one minute they told the whole story:
- 'rxB' (bytes received) rose by exactly the size of the six frames the LoRa Pico sent.
- 'frames' stayed frozen at 4971.
- 'buf' (unparsed bytes) climbed 6787 → 7246 → 8162.
- 'bad' and 'parse_err' didn't move, so nothing was being rejected.
- 'sent_age' crept up to 95 seconds.
So the bytes were arriving, and the parser just wasn't getting through them.
Root cause
About 8% of UART frames arrive damaged, most likely overruns while the Wiznet is busy on the network. When a damaged payload hit _decode_payload, payload.decode('utf-8') threw a UnicodeError. The fallback, decode('utf-8', 'ignore'), doesn't help, because this MicroPython version ignores the errors argument and throws again. The exception was swallowed, the read position never moved, and every frame behind the bad one waited until the 8 KB buffer limit finally cut it off. Then the whole backlog went out in one burst.
A quick simulation of the parser reproduced it: bursts of up to 35 frames with the old code, and every frame delivered immediately with the fix.
The fix
- _decode_payload returns None on a bad payload instead of clogging the buffer.
- If the next frame's A5 5A header shows up inside the current frame's length, the current frame is garbled, so drop it immediately and resync at that header.
- After any skip, the parse loop keeps draining instead of waiting for the next read.
- Any other parse exception drops one byte and resyncs, and is counted in a new pexc field in the heartbeat.
A damaged frame now costs one packet instead of stalling everything behind it.
Result
After flashing, the Wiznet heartbeat showed frames: 19, sent: 19, buf: 0, bad: 0, pexc: 0. A sensor packet ACKed at 7:36:16 was on the websocket before the 7:36:19 heartbeat. No more batching.
And the battery test that started all this is now running for real.
Link to the fix
https://github.com/scottlecomte/Lora-to-Ethernet-Bridge
Moisture sensor project to be created soon!
Scott LeComte





