← Projects
electronics advanced Jul 15, 2026 ◑ 2 views ◯ 5 min read

Scaling Up: A Multi-Node LoRa Sensor Network with a Raspberry Pi Gateway

Build time: Multiple weekends
Tools needed: Soldering iron
Parts List
esp32raspberry piloramesh networkinfluxdbgrafanasensor networkcombo project

Our LoRa sensor node guide covered a single node reporting to a single gateway — the simplest useful LoRa setup. This project scales that pattern to a full property-wide sensor network: multiple ESP32 nodes scattered across buildings and outdoor areas, all reporting through a Raspberry Pi gateway into one central dashboard.

When You Actually Need a Mesh/Multi-Node Network

A single node-to-gateway link is fine for one sensor. Once you're covering multiple outbuildings, garden zones, or a property large enough that a single gateway doesn't have clean line-of-sight to every node, you're into genuine network design territory — node addressing, a central data pipeline, and a dashboard that can handle an arbitrary, growing number of sensors rather than one hardcoded endpoint.

Architecture

LayerComponentRole Sensor nodesMultiple ESP32 + LoRa boards, battery/solar poweredEach monitors its local sensors (temperature, soil moisture, door state, whatever's relevant to its location) and transmits periodically with a unique node ID GatewayRaspberry Pi + LoRa HAT/moduleListens continuously for LoRa packets from all nodes, parses and forwards them into persistent storage Storage/dashboardInfluxDB + Grafana on the same Pi, or Home AssistantTime-series storage and visualization — if you've built our Pi weather station, this reuses the exact same InfluxDB/Grafana pattern for a different data source

Node Addressing at Scale

With multiple nodes, consistent identification is essential — extend the single-node pattern with a structured payload rather than the simple string concatenation that's fine for one node:

#include <ArduinoJson.h> void sendReading(String nodeId, float temperature, float moisture, int battery) { JsonDocument doc; doc["node"] = nodeId; doc["temp"] = temperature; doc["moisture"] = moisture; doc["battery"] = battery; doc["timestamp"] = millis(); // or real time if node has RTC/NTP sync String payload; serializeJson(doc, payload); LoRa.beginPacket(); LoRa.print(payload); LoRa.endPacket(); }

JSON payloads cost slightly more airtime than raw string concatenation, but the reliability and extensibility gain (easy to add new fields per-node without breaking a fragile parsing scheme) is worth it once you're managing more than a couple of nodes.

Gateway: Parsing and Routing

The Pi gateway's job expands from "forward one stream" to "receive from many nodes, parse, and store each appropriately":

import serial import json from influxdb_client import InfluxDBClient, Point client = InfluxDBClient(url="http://localhost:8086", token="your-token", org="your-org") write_api = client.write_api() ser = serial.Serial('/dev/ttyUSB0', 115200) # adjust to your LoRa gateway's actual interface while True: line = ser.readline().decode('utf-8').strip() if not line: continue try: data = json.loads(line) point = ( Point("sensor_reading") .tag("node", data["node"]) .field("temperature", data["temp"]) .field("moisture", data.get("moisture", 0)) .field("battery", data["battery"]) ) write_api.write(bucket="lora-sensors", record=point) except (json.JSONDecodeError, KeyError) as e: print(f"Skipped malformed packet: {line}")

Tagging each point with the node ID (rather than putting it in a separate field) is what lets Grafana later filter/group by node cleanly — InfluxDB tags are indexed for exactly this kind of query pattern.

Handling Range Gaps: Repeater Nodes

If your property has a dead zone the gateway can't reach directly — a detached building behind other structures, for instance — a simple repeater node can bridge the gap: a node that listens for packets from far nodes and immediately re-transmits them, effectively extending range at the cost of some added latency and complexity.

void loop() { int packetSize = LoRa.parsePacket(); if (packetSize) { String received = ""; while (LoRa.available()) { received += (char)LoRa.read(); } // Only re-transmit if this looks like a valid sensor payload, // to avoid a repeater re-broadcasting noise or malformed packets if (received.startsWith("{")) { delay(100); // small delay to avoid immediate collision with the original transmission LoRa.beginPacket(); LoRa.print(received); LoRa.endPacket(); } } }

This is a genuinely simple repeater pattern, not true mesh routing — for larger, more complex networks, established mesh protocols (Meshtastic is a popular pre-built option worth investigating if you outgrow simple point-to-point/repeater patterns) handle multi-hop routing more robustly than a hand-rolled repeater.

Dashboard: Visualizing Multiple Nodes

With node-tagged data in InfluxDB, Grafana panels can filter and group by node dynamically — a single "Temperature by Location" panel showing all nodes as separate lines, rather than needing a hand-built dashboard per sensor. This is where the JSON-with-tags approach from earlier pays off directly: adding a new node to the network means it just appears as a new option in your existing dashboard's node filter, no dashboard rebuilding required.

Power Planning Across a Whole Network

Node TypePower Approach Near-building nodesWired power where practical — removes battery maintenance entirely for easily-reachable locations Remote/outdoor nodesBattery + solar trickle charge, using the deep-sleep pattern from the single-node guide Repeater nodesWired power strongly preferred if feasible — a repeater going offline breaks connectivity for every node behind it, so battery-only repeaters are a network reliability risk

Scaling Checklist

  1. Start with 2–3 nodes and confirm the full pipeline (node → gateway → InfluxDB → Grafana) works end to end before scaling up
  2. Add nodes incrementally, verifying each new node's packets are being received and correctly tagged before moving to the next
  3. Monitor for packet collisions as node count grows — LoRa is a shared medium, and enough simultaneously-transmitting nodes can start colliding; staggering transmission intervals slightly per-node (rather than having every node transmit on the exact same schedule) reduces this
  4. Add repeater nodes only where range testing actually shows a gap, not preemptively — unnecessary repeaters add complexity and airtime congestion without benefit

The step from one sensor to a real property-wide network is mostly about data structure and dashboard design rather than fundamentally new radio concepts — get the JSON-tagged-payload pattern right early, and adding node #12 is exactly as easy as adding node #2 was.