<!--order=9-->


<h1>Raspberry Pi 5 as a Meshtastic Gateway</h1>
<p>A Raspberry Pi 5 makes an excellent Meshtastic gateway — a node that bridges the mesh to the internet, logs data from field sensors, and gives you remote visibility into your network. The Pi runs the Meshtastic Python library to talk to an attached LoRa module (or connects via USB to a Pico running Meshtastic), then forwards traffic to MQTT and logs telemetry to a local database. This is the architecture for a base station at the edge of a field network.</p>

<h2>Architecture</h2>
<pre>
Field Nodes (Pico + SX1262, battery powered)
  → LoRa mesh (Meshtastic, encrypted)
  → Gateway Node (Pi 5 + LoRa module or USB Pico)
      → meshtastic Python library
      → MQTT broker (local or cloud)
      → InfluxDB / SQLite (local storage)
      → Grafana (visualization)
      → Optional: forward to remote server via SSH tunnel / VPN

The Pi 5 stays powered (AC or large solar), so it can act as a
permanent relay node and always-on data logger.
</pre>

<h2>Choosing a Pi 5 Role</h2>
<p>Both 4 GB and 16 GB Pi 5 boards can be useful gateways. The radio interface and a small SQLite logger need very little memory; storage quality, reliable power, and a good antenna matter more.</p>
<table>
  <thead><tr><th>Pi 5 role</th><th>4 GB board</th><th>16 GB board</th></tr></thead>
  <tbody>
    <tr><td>USB radio + Meshtastic CLI</td><td>More than enough</td><td>More than enough</td></tr>
    <tr><td>SQLite logger + SSH or VPN access</td><td>Good fit</td><td>Good fit</td></tr>
    <tr><td>Mosquitto + modest dashboard</td><td>Usually fine with sensible limits</td><td>Extra headroom</td></tr>
    <tr><td>Several services, Grafana, databases, development work</td><td>Possible, but monitor memory</td><td>Better choice</td></tr>
  </tbody>
</table>
<p>Use a quality USB-C power supply and, for a long-lived logger, an SSD or other storage designed for repeated writes. A fast board with a brownout-prone supply or worn microSD card is not a reliable gateway.</p>

<h2>Hardware Options for the Gateway Radio</h2>

<table>
  <thead><tr><th>Option</th><th>Notes</th></tr></thead>
  <tbody>
    <tr><td>USB-connected Pico + SX1262</td><td>Simplest: flash Meshtastic onto the Pico, plug into Pi via USB. Python CLI connects via serial. No HAT needed on the Pi.</td></tr>
    <tr><td>Waveshare SX1262 HAT for Pi</td><td>Plugs onto Pi's 40-pin header. Requires SPI configuration on the Pi. More integrated.</td></tr>
    <tr><td>RAK2287 / RAK5146 M.2 card</td><td>LoRaWAN concentrator for Pi via M.2. Multi-channel (8+ simultaneous). Overkill for Meshtastic but excellent for LoRaWAN.</td></tr>
  </tbody>
</table>

<h2>Setup: USB Pico as Gateway Radio</h2>
<pre>
# On the Pico (already flashed with Meshtastic):
# Configure as a router to participate actively in relaying
$ meshtastic --port /dev/ttyACM0 --set device.role ROUTER

# On the Pi 5:
$ sudo apt update && sudo apt install python3-pip python3-venv -y
$ python3 -m venv ~/meshtastic-env
$ source ~/meshtastic-env/bin/activate
$ pip install meshtastic

# Verify the Pico is visible
$ meshtastic --port /dev/ttyACM0 --info

# Add your user to the dialout group (avoid sudo for serial access)
$ sudo usermod -aG dialout $USER
$ newgrp dialout
</pre>

<h2>Use a Stable Serial Device Name</h2>
<p><code>/dev/ttyACM0</code> can change when another USB serial device is connected or after a reboot. On a Pi gateway, prefer the stable symlink created for the physical Pico:</p>
<pre>
$ ls -l /dev/serial/by-id/
usb-Raspberry_Pi_Pico_... -&gt; ../../ttyACM0

# Use the full /dev/serial/by-id/... path in scripts and systemd units.
</pre>
<p>Update <code>SERIAL_PORT</code> in the logger to that stable path after confirming it works. This makes an unattended gateway much less likely to attach to the wrong device.</p>

<h2>Python Script: Logging Telemetry to SQLite</h2>
<pre>
#!/usr/bin/env python3
"""Log Meshtastic telemetry to SQLite."""

import sqlite3
import time
import meshtastic
import meshtastic.serial_interface
from pubsub import pub

DB_PATH = "/home/pi/meshtastic-data.db"
SERIAL_PORT = "/dev/ttyACM0"

def init_db():
    con = sqlite3.connect(DB_PATH)
    cur = con.cursor()
    cur.execute("""
        CREATE TABLE IF NOT EXISTS telemetry (
            id          INTEGER PRIMARY KEY AUTOINCREMENT,
            ts          INTEGER NOT NULL,
            node_id     TEXT NOT NULL,
            long_name   TEXT,
            battery_pct REAL,
            voltage     REAL,
            rssi        INTEGER,
            snr         REAL
        )
    """)
    cur.execute("""
        CREATE TABLE IF NOT EXISTS messages (
            id       INTEGER PRIMARY KEY AUTOINCREMENT,
            ts       INTEGER NOT NULL,
            from_id  TEXT NOT NULL,
            to_id    TEXT,
            channel  INTEGER,
            text     TEXT
        )
    """)
    con.commit()
    return con

con = init_db()

def on_receive(packet, interface):
    ts = int(time.time())
    from_id = hex(packet.get('from', 0))
    decoded = packet.get('decoded', {})
    portnum = decoded.get('portnum', '')

    if portnum == 'TELEMETRY_APP':
        tel = decoded.get('telemetry', {}).get('deviceMetrics', {})
        rssi = packet.get('rxRssi')
        snr  = packet.get('rxSnr')
        cur = con.cursor()
        cur.execute(
            "INSERT INTO telemetry (ts, node_id, battery_pct, voltage, rssi, snr) "
            "VALUES (?, ?, ?, ?, ?, ?)",
            (ts, from_id,
             tel.get('batteryLevel'),
             tel.get('voltage'),
             rssi, snr)
        )
        con.commit()
        print(f"[{ts}] Telemetry from {from_id}: "
              f"batt={tel.get('batteryLevel')}% rssi={rssi}")

    elif portnum == 'TEXT_MESSAGE_APP':
        text = decoded.get('text', '')
        cur = con.cursor()
        cur.execute(
            "INSERT INTO messages (ts, from_id, channel, text) VALUES (?, ?, ?, ?)",
            (ts, from_id, packet.get('channel', 0), text)
        )
        con.commit()
        print(f"[{ts}] Message from {from_id}: {text}")

pub.subscribe(on_receive, "meshtastic.receive")

iface = meshtastic.serial_interface.SerialInterface(SERIAL_PORT)
print("Gateway running. Ctrl+C to stop.")

try:
    while True:
        time.sleep(1)
except KeyboardInterrupt:
    iface.close()
    con.close()
    print("Stopped.")
</pre>

<h2>MQTT Forwarding from the Pi</h2>
<p>A USB-connected Pico talks to the Pi over serial; it cannot reach the Pi's <code>localhost</code> MQTT broker on its own. Let the Python process running on the Pi publish the packets it receives.</p>
<pre>
# Install a local broker on the Pi
$ sudo apt install mosquitto mosquitto-clients -y
$ sudo systemctl enable --now mosquitto

# In the same Python virtual environment as gateway.py:
$ python -m pip install paho-mqtt
</pre>
<pre>
# Add a small publisher to gateway.py (this code runs on the Pi):
import json
import paho.mqtt.publish as publish

def publish_local(topic, payload):
    publish.single(
        topic,
        payload=json.dumps(payload),
        hostname="127.0.0.1",
    )

# Call it from on_receive after validating and storing a packet:
# publish_local("field/telemetry", decoded)
# publish_local("field/text", decoded)
</pre>
<pre>
# Watch the local messages:
$ mosquitto_sub -h 127.0.0.1 -t "field/#" -v
</pre>
<p>Bind a private broker to the loopback interface unless another protected machine truly needs access. If MQTT must leave the Pi, add authentication, TLS, and a clear topic policy. A Pico W can use a reachable network broker when configured for Wi‑Fi, but a plain Pico cannot.</p>

<h2>Running the Logger as a systemd Service</h2>
<pre>
# /etc/systemd/system/meshtastic-gateway.service

[Unit]
Description=Meshtastic Gateway Logger
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi
ExecStart=/home/pi/meshtastic-env/bin/python3 /home/pi/gateway.py
Restart=on-failure
RestartSec=10

[Install]
WantedBy=multi-user.target
</pre>
<p>Keep the serial device name, database path, and log location in one documented place. Watch the service with <code>journalctl</code> after every reboot and after reconnecting the USB radio.</p>
<pre>
$ sudo systemctl daemon-reload
$ sudo systemctl enable meshtastic-gateway
$ sudo systemctl start meshtastic-gateway
$ sudo systemctl status meshtastic-gateway
$ journalctl -u meshtastic-gateway -f   # live logs
</pre>

<h2>Querying Logged Data</h2>
<pre>
$ sqlite3 /home/pi/meshtastic-data.db

# Recent telemetry from all nodes
SELECT datetime(ts, 'unixepoch', 'localtime') AS time,
       node_id, battery_pct, voltage, rssi, snr
FROM telemetry
ORDER BY ts DESC
LIMIT 20;

# Average battery per node
SELECT node_id,
       round(avg(battery_pct), 1) AS avg_batt,
       round(min(battery_pct), 1) AS min_batt,
       count(*) AS samples
FROM telemetry
WHERE ts > strftime('%s', 'now') - 86400  -- last 24 hours
GROUP BY node_id;

# Recent messages
SELECT datetime(ts, 'unixepoch', 'localtime'), from_id, text
FROM messages
ORDER BY ts DESC LIMIT 10;
</pre>

<h2>Remote Access to the Gateway</h2>
<pre>
# SSH tunnel — forward Pi's port 8080 (Grafana, etc.) to local machine
$ ssh -L 8080:localhost:3000 pi@pi5-gateway.local

# Or use a VPN (WireGuard) for persistent remote access:
$ sudo apt install wireguard
# Configure WireGuard to create a permanent tunnel to a VPS
# Then access the Pi via its WireGuard IP from anywhere

# Tailscale — easiest option for remote access without VPS:
$ curl -fsSL https://tailscale.com/install.sh | sh
$ sudo tailscale up
# Pi is now accessible from anywhere via Tailscale's mesh VPN
</pre>

<h2>Operational Checks</h2>
<ul>
  <li>Confirm the gateway sees the expected USB radio after each reboot.</li>
  <li>Alert on a missing telemetry report instead of only checking for process uptime.</li>
  <li>Check database size and free disk space; logs should not consume the system disk indefinitely.</li>
  <li>Keep a backup of the database and the Meshtastic configuration separately.</li>
  <li>Apply operating-system and Meshtastic updates deliberately, with a rollback note for field hardware.</li>
</ul>
<p>The most useful gateway is boring: it restarts after a power cut, identifies its radio consistently, stores its data safely, and makes an outage obvious.</p>

<h2>References</h2>
<ul>
  <li><a href="https://meshtastic.org/docs/software/python/installation/" target="_blank" rel="noopener">Meshtastic Python Library ↗</a></li>
  <li><a href="https://meshtastic.org/docs/configuration/module/mqtt/" target="_blank" rel="noopener">Meshtastic MQTT Configuration ↗</a></li>
  <li><a href="https://tailscale.com/kb/1187/install-raspberrypi/" target="_blank" rel="noopener">Tailscale on Raspberry Pi ↗</a></li>
</ul>
