Skip to content

Observer Node: Raspberry Pi + Wio SX1262

A cheap Raspberry Pi paired with a Wio SX1262 LoRa module makes a capable MeshCore observer you can leave plugged in anywhere there's power and WiFi. Once deployed, it requires no hands-on access - you manage it remotely over SSH.

Affiliate links

Amazon links on this page use our affiliate tag. You pay the same price - commissions go directly toward hardware for CSRA community relay nodes.


What It Does

This build is aimed at locations where you want a persistent, low-maintenance presence on the mesh without leaving an expensive node unattended. A few good uses:

  • Feed MQTT observer networks like letsmesh.net and Corescope with live mesh data
  • Host meshcore-bot for automated mesh testing
  • Provide a remote WiFi companion endpoint for testing new configs
  • Stay accessible over SSH via WireGuard even after deployment

Parts List

Prices as of Feb 2026.

Part Price
Raspberry Pi Zero 2W ~$18 Amazon   Digikey
Wio SX1262 Dev Board ~$5 Seeed Studio
64GB microSD card ~$12 Amazon
5V 2.5A microUSB power supply ~$8 Amazon
Female-to-female jumper wires ~$2 Amazon

Total: ~$45

Any Pi works

The Zero 2W is the lowest-cost option and plenty capable for this use case. Any larger Pi (3, 4, 5) will work fine - the wiring and software setup is identical. A bigger Pi may already be sitting in a drawer.

Get the version with header pins

The Pi Zero 2W is sold with and without GPIO header pins soldered. Get the version with headers pre-soldered - it saves a soldering step and makes attaching jumper wires straightforward.

You'll also want a small plastic enclosure - since this is an indoor build with no weatherproofing requirements, a basic project box works fine.


Wiring the Pi and SX1262

The SX1262 connects to the Pi's SPI bus plus a few GPIO pins for control signals. The table below uses the Wio SX1262 pin numbering where R1 is the top-right pin and L1 is the top-left.

SX1262 Pin Function Pi Pin BCM GPIO
R2 GND Ground Pin 20 -
R3 3V3 3.3V Power Pin 17 -
R4 MOSI SPI MOSI Pin 19 GPIO10
R5 MISO SPI MISO Pin 21 GPIO9
R6 SCK SPI Clock Pin 23 GPIO11
L2 DIO1 IRQ Pin 36 GPIO16
L3 NRST Reset Pin 37 GPIO26
L4 BUSY Busy Pin 38 GPIO20
L5 NSS SPI CE0 Pin 40 GPIO21

Pin references

A pinout diagram for the Pi Zero 2W is available at pinout.xyz. The SX1262 board's silkscreen labels match the table above.

Pi Zero 2W wired to Wio SX1262 via jumper wires


Setting Up the SD Card

Use Raspberry Pi Imager to flash the card. Under OS, choose Raspberry Pi OS (other) - Raspberry Pi OS Lite (64-bit).

In the imager's customization settings before writing:

  • Enter your WiFi credentials
  • Set a hostname
  • Add your SSH public key for access

Configuring the Pi

Pop the SD card in, power up, and find the Pi's IP on your network. SSH in, then run:

sudo raspi-config

Navigate to Interface Options - SPI - Yes to enable SPI for the SX1262.

Then install packages:

sudo apt update && sudo apt install -y vim procps jq rpi-usb-gadget openresolv wireguard docker.io docker-compose

Enable USB gadget mode (lets you SSH over a direct USB cable if you need to reconfigure a deployed unit without network access):

sudo rpi-usb-gadget on

Add the pi user to the docker group, then reboot to apply SPI, gadget, and group changes:

sudo usermod -aG docker pi
sudo shutdown -r now

Software

WireGuard

If you already run a WireGuard server, generate a new client config and copy it to the Pi at /etc/wireguard/wg.conf. Tweak [Peer] AllowedIPs to limit the tunnel to only your VPN subnet (e.g. 10.8.0.0/24) - there's no reason this device needs a route to the wider internet through the tunnel.

sudo systemctl enable wg-quick@wg
sudo systemctl start wg-quick@wg

Verify connectivity using the WireGuard IP rather than the local IP once it's up.

Untrusted device consideration

These units may be in locations you don't fully control. Consider treating them as untrusted clients on your VPN - restricting them so they can only respond to inbound connections rather than initiate new ones. On a wg-easy setup, you can do this with iptables rules in WG_POST_UP/WG_POST_DOWN:

WG_POST_UP=iptables -I FORWARD -i wg0 -s 10.8.0.9 -m conntrack --ctstate NEW -j DROP; iptables -I INPUT -i wg0 -s 10.8.0.9 -m conntrack --ctstate NEW -j DROP
WG_POST_DOWN=iptables -D FORWARD -i wg0 -s 10.8.0.9 -m conntrack --ctstate NEW -j DROP; iptables -D INPUT -i wg0 -s 10.8.0.9 -m conntrack --ctstate NEW -j DROP

Replace 10.8.0.9 with the actual IP allocated to your device. These rules need to be updated for each new unit.


Captive Portal Handling (optional)

Some locations use a captive portal that requires a browser-style login before granting internet access. The script below handles one particular portal type - you'll likely need to customize it for your location. Skip this section if your deploy site has open WiFi.

The script is written to run every minute from cron but throttles itself: it checks every minute for the first 10 minutes after boot, then backs off to every 10 minutes.

/home/pi/connect.py

#!/usr/bin/env python3

"""
Check for a captive portal and log in to gain internet connectivity.
Runs from cron every minute but throttles after first 10 minutes of uptime.
"""

import requests
import re
import sys
import time

def get_uptime_seconds():
    with open('/proc/uptime', 'r') as f:
        return float(f.readline().split()[0])

try:
    with open('last_connect_run.txt', 'r') as f:
        last_run = float(f.readline().strip())
        time_since_last_run = time.time() - last_run
except:
    time_since_last_run = 99999999

if get_uptime_seconds() > 60 * 10:
    if time_since_last_run < 60 * 10:
        print(f'only {time_since_last_run:.0f} seconds since last run...waiting...')
        sys.exit(0)

with open('last_connect_run.txt', 'wt') as f:
    f.write(str(time.time()))

session = requests.Session()
headers = {"User-Agent": "Mozilla/5.0 Chrome/145 Safari/537.36"}
test_url = "http://google.com"

print("STEP 1: trigger captive portal")
r = session.get(test_url, headers=headers, allow_redirects=False)
login_url = re.search(r"LoginURL>(.*?)</LoginURL>", r.text)
if not login_url:
    print("no captive portal found - done!")
    sys.exit(0)
login_url = login_url.group(1)
print("Login URL:", login_url)

print("STEP 2: load splash page")
r2 = session.get(login_url, headers=headers)

print("STEP 3: call grant")
grant_url = re.sub(r"\?.*", "", login_url) + "grant?continue_url=http://google.com"
r3 = session.get(grant_url, headers={**headers, "Referer": login_url})
print("Grant status:", r3.status_code)

Make it executable:

chmod +x /home/pi/connect.py

Add to the pi's crontab (sudo crontab -e):

* * * * * ping -c 1 -W 2 10.8.0.1
* * * * * /home/pi/connect.py

Note

The ping line keeps the WireGuard tunnel alive by generating outbound traffic. This is a workaround for a firewall config issue and may not be needed in your setup.


pyMC_Repeater

pyMC_Repeater is the core software - it handles communication with the SX1262 over SPI and provides a companion TCP interface that meshcore-bot and other tools can connect to.

git clone https://github.com/rightup/pyMC_Repeater.git
cd pyMC_Repeater
git checkout dev
cp config.yaml.example config.yaml

Edit config.yaml. The SX1262 section should match your wiring from the table above:

sx1262:
  bus_id: 0
  busy_pin: 20
  cs_id: 0
  cs_pin: 21
  dio3_tcxo_voltage: 1.8
  irq_pin: 16
  is_waveshare: true
  reset_pin: 26
  rxen_pin: 12
  rxled_pin: -1
  txen_pin: 13
  txled_pin: -1
  use_dio2_rf: false
  use_dio3_tcxo: true

Update docker-compose.yml to expose the SPI and GPIO devices:

services:
  pymc-repeater:
    build: .
    container_name: pymc-repeater
    restart: unless-stopped
    ports:
      - 8000:8000
      - 5000:5000
      - 5001:5001
    devices:
      - /dev/spidev0.0
      - /dev/spidev0.1
      - /dev/gpiochip0
      - /dev/gpiochip4
      - /dev/gpiomem
    cap_add:
      - SYS_RAWIO
    group_add:
      - plugdev
    volumes:
      - ./config.yaml:/etc/pymc_repeater/config.yaml
      - ./data:/var/lib/pymc_repeater

Start it:

docker-compose up -d
docker-compose logs -f

meshcore-bot

meshcore-bot connects to the companion interface exposed by pyMC_Repeater and provides automated mesh testing and bot functionality.

cd ~
git clone https://github.com/agessaman/meshcore-bot.git
cd meshcore-bot
mkdir -p data/{backups,config,databases,logs}
cp config.ini.minimal-example data/config/config.ini
ln -s data/config/config.ini .

Edit config.ini:

[Connection]
connection_type = tcp
hostname = 127.0.0.1
tcp_port = 5000

[Bot]
bot_name = YourNode-bot
advert_interval_hours = 1
startup_advert = zero-hop

In docker-compose.yml, uncomment the network_mode: host block and remove the :ro suffix on the config volume mount. Then start it the same way as pyMC_Repeater.


Preparing for Deployment

Before taking the unit to its final location, add the destination's WiFi credentials:

sudo nmtui

Set the new network to auto-connect. If you're deploying to multiple locations, add all of them - the Pi will connect to whichever is available.

Also confirm that the WireGuard [Peer] Endpoint address is reachable from the deploy location's network, not just your home network.


Result

Completed observer unit in clear plastic enclosure, plugged into power

Plug the unit in, walk away, and SSH in via WireGuard whenever you need to make changes. The pyMC_Repeater and bot services restart automatically on reboot, and the captive portal script (if needed) handles reconnection after power cycles.

This is one example deployment

The steps above reflect a specific setup - particular software choices, a WireGuard server already in place, and deploy locations with a known captive portal. Your situation will likely differ. Treat this as a starting point: some sections (WireGuard, captive portal handling, meshcore-bot) may not apply to you at all, and others may need adjustment for your network environment, hosting preferences, or intended use.