Meshtastic, Satdump and SDR++ server box with a PoE-powered Raspberry Pi

In this article I will explain how to build a PoE powered Raspberry Pi box, with three RTL-SDR v4’s and a shield with two Wio SX1262 LoRa modules for Meshtastic. It runs dockerized Satdump, an SDR++ server for remote listening, and two Meshtastic nodes.

What are Meshtastic, SDR++ or Satdump?
  • Meshtastic is a project that enables you to use inexpensive LoRa radios as a long range off-grid communication platform in areas without existing or reliable communications infrastructure. This project is 100% community driven and open source! (Source)
  • SDR++ (pronounced SDR plus plus) is a cross-platform and open source SDR software with the aim of being bloat free and simple to use. (Source)
  • Satdump is a general purpose satellite data processing software. It is a one-stop-shop that provides all the necessary stages to get from the satellite transmission to actual products. (Source)

Why I built this

The only place where I can put antennas is on the flat roof of my house. While it’s covered in solar panels, there was still some space left to put up a mast. However, I didn’t look forward to running down coax for each individual antenna, so I quickly realized I needed some box on the roof where all the antennas would connect to. That way I could run just a single ethernet cable down. You can then listen to the SDRs using your LAN, or even remotely via VPN!

My requirements for the box were:

  • Two Meshtastic transceivers: I wanted an omnidirectional antenna and a Yagi.
  • Three RTL-SDR v4’s: One will be connected to my Diamond X50N UHF/VHF antenna for general listening. The second one for the 137.5 MHz QFH antenna I built years ago during Covid, to receive images from weather satellites. The third one is still unused, but will probably be used for either ADS-B (planes) or AIS (boats).
  • A Raspberry Pi 4 for the brains. Of course the RPi 5 is newer, but it also uses a lot more power and might simply be overkill.
  • All powered via Power-over-Ethernet (PoE) by my Ubiquiti switch in the basement.

These are actual sketches I made in June this year, to plan it all out:

And this is what I ended up installing:

In this article we will not focus on the antennas, but rather what they connect to.

Hardware

Shopping list

I gathered the parts from different places. Below I’ve added links to all the shops. Some of them are affiliate links.

  • A waterproof enclosure. This box is going to live permanently on the roof, so I wanted it to be waterproof. The electronics inside will generate some heat, so I opted for a metal case.
  • A Raspberry Pi 4 (RPi) with heat sink and fans.
  • Waveshare 5V/5A PoE splitter. Waveshare make some amazing devices. I chose this one because 25W ought to be enough power for the RPi and everything connected. It’s also built out of metal, so should dissipate heat easily. My only gripe is that the ethernet and USB leads are a little too short.
  • 3x RTL-SDR v4, bought from the official RTL-SDR Blog AliExpress store.
  • Short USB cables with ferrite beads to connect the SDRs to the RPi. Not sure the ferrite beads matter that much for such a short distance, but it shouldn’t hurt either.
  • 2x Wio SX1262 modules with IPEX connector. This was the most elegant solution I found to add LoRa to an RPi. I had to design my own shield and had the PCB made by PCBWAY. More on this later.
  • 5x N-Type female to SMA male bulkhead connectors. These can cost you an arm and a leg in Europe, but if you can wait then AliExpress is once again a good source. I prefer N-type over SMA for anything that’s exposed to the elements.
  • IPEX to SMA female coax to connect the Wio SX1262 to the bulkhead connector.
  • Waterproof RJ45 connector to bring power and internet into the box.
  • A fifty centimeter ethernet cable to connect the PoE splitter to the waterproof RJ45 connector. Cat5e is more than enough.

The box

The waterproof box has a metal cover at the bottom that can be removed. An N-type bulkhead connector needs five holes. Instead of trying to drill these holes, I decided to just 3D print a new cover with the holes already in place. I could repurpose the rubber ring that came with the original cover. After installing the connectors, I applied plenty of hot glue on the inside, to help it further seal off from any rain that might try to get in.

Drilling the hole for the RJ45 connector was a different feat. I bought a 25 mm drill bit that supposedly would work for steel, but it continuously got stuck while drilling. I can’t recommend it, so won’t link to the Amazon page. In the end I managed to create a hole, albeit scruffy. If anyone got tips how to cleanly drill 25 mm holes in thick steel, please let me know 😄

I also drilled a few holes in the mounting plate in the box, to run some zip ties through for attaching the different components.

Raspberry Pi Meshtastic shield

This was by far the most challenging part: how do you turn a Raspberry Pi into a Meshtastic node?

There is a page on the Meshtastic website that describes how to run it under Linux using Portduino. For hardware, they seem to favor the “Meshstick“, which is basically a USB-to-SPI interface for an SX1262 LoRa module. To me this seemed a clunky solution, because the RPi already has its SPI pins exposed and I needed my USB ports for the RTL-SDRs.

I decided to design my own RPi shield in KiCad. It has a 5V to 3.3V power converter and two SX1262 modules. My first prototype had only the SPI pins connected, but it turned out it also needs the remaining pins broken out, so I had to add some extra air wires:

Version 0.2 has incorporated the fixes, has a power LED in Meshtastic green and it also has a connector to power the fans for the RPi heat sink:

Just look at that beautiful green light 😍

Software

I will split this section up between the RTL-SDRs and Satdump/SDR++ containers, and the setup of the Meshtastic nodes. I have also added two tricks for Meshtastic: announcing the nodes to the LAN and automatically updating the Docker containers.

Satdump and SDR++

RTL-SDR drivers and serial number

The version 4 of RTL-SDR requires different drivers from v3! Make sure to follow the steps here to install the right drivers.

You will make your live a lot easier when you give the RTL-SDRs different serial numbers. That will help you identify them in SDR++ or Satdump. Attach the different RTL-SDRs one by one and execute the following commands, each time with a unique serial number:

sudo rtl_eeprom -s 00123000

SDR++ server

I wanted to run a Docker container with the RTL-SDR v4 drivers and SDR++, so I made one. The code can be found on Github and the image is available on Docker Hub. You run the container with this docker-compose:

services:
  sdrpp-server:
    container_name: sdrpp-server
    image: nikotine1/sdrpp-server:latest
    restart: unless-stopped
    privileged: true
    environment:
      - PORT=5555
    ports:
      - "5555:5555"

I tried hard to run this container without priviliged: true and instead share just one RTL-SDR, but the RTL-SDR drivers would not have it. They would still show the other SDRs as well.

Now it’s just a matter of selecting SDR++ server as a source and RTL-SDR as remote source. You’ll see the SDRs with the easy to recognize serial numbers you set in the chapter above. Now you can listen to your RTL-SDRs via wifi while sitting in your sofa.

Satdump

Satdump can decode a plethora of satellite signals. It can be setup so that it runs standalone, tracking and decoding the satellites as they pass by. I created a Docker container which can be run with this docker-compose:

services:
  satdump:
    container_name: satdump
    image: nikotine1/satdump:latest
    restart: unless-stopped
    privileged: true
    ports:
      - "8881:8081"
    volumes:
      - /home/pi/docker/satdump/config:/satdump/config
      - /home/pi/docker/satdump/output:/satdump/output
      - /etc/localtime:/etc/localtime:ro

  nginx:
    container_name: nginx
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - /home/pi/docker/satdump/output:/usr/share/nginx/html:ro
      - /home/pi/docker/satdump/nginx/nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - /etc/localtime:/etc/localtime:ro

The Satdump Docker image has been made so that it looks for the file autotrack.json at startup, stored in the satdump/config folder. This is my file:

{
    "parameters": {
        "source": "rtlsdr",
        "source_id": "25092002",
        "samplerate": 1.024e6,
        "initial_frequency": 137.5e6,
        "gain": 49.6,
        "agc": false,
        "lna_agc": false,
        "tuner_agc": false,
        "bias": false,
        "ppm_correction": 0,
        "fft_enable": true,
        "fft_size": 8192,
        "fft_rate": 10,
	    "fft_avgn": 10,
        "fft_min": -110,
        "fft_max": 0
    },
    "finish_processing": true,
    "output_folder": "/satdump/output",
    "qth": {
        "lat": xx.xxxxxxxxxxxx,
        "lon": xx.xxxxxxxxxxx,
        "alt": xx
    },
    "http_server": "0.0.0.0:8081",
    "tracking": {
        "rotator_algo": {
            "meridian_flip_correction": false,
            "park_position": {
                "az": 0.0,
                "el": 0.0
            },
            "park_while_idle": false,
            "rotator_arrowkeys_enable": false,
            "rotator_az_max": 360,
            "rotator_az_min": 0,
            "rounding": false,
            "rounding_decimal_places": 3,
            "unpark_at_minus": 60.0,
            "update_period": 1.0
        },
        "autotrack_cfg": {
            "autotrack_min_elevation": 40.0,
            "stop_sdr_when_idle": true,
            "multi_mode": false,
            "use_localtime": true
        },
        "rotator_config": {
            "rotctl": {
                "address": "127.0.0.1",
                "port": 4533
            }
        }
    },
    "tracked_objects": [
        {
            "norad": 57166,
            "min_elevation": 20.0,
            "downlinks": [
                {
                    "frequency": 137900000,
                    "record": false,
                    "live": true,
                    "pipeline_name": "meteor_m2-x_lrpt",
                    "pipeline_params": {
                        "dc_block": true,
                        "fill_missing": true,
                        "freq_shift": 0,
                        "iq_swap": false,
                        "rs_usecheck": true,
                        "satellite_number": "M2-3"
                    },
                    "baseband_format": "cs16",
                    "baseband_decimation": 1
                }
            ]
        },
        {
            "norad": 59051,
            "min_elevation": 20.0,
            "downlinks": [
                {
                    "frequency": 137900000,
                    "record": false,
                    "live": true,
                    "pipeline_name": "meteor_m2-x_lrpt",
                    "pipeline_params": {
                        "dc_block": true,
                        "fill_missing": true,
                        "freq_shift": 0,
                        "iq_swap": false,
                        "rs_usecheck": true,
                        "satellite_number": "M2-4"
                    },
                    "baseband_format": "cs16",
                    "baseband_decimation": 1
                }
            ]
        }
    ]
}

The images that Satdump decodes are saved in the satdump/output folder. To easily view them in a browser, I also run an nginx webserver container. This is the nginx.conf file:

server {
  listen 80;
  server_name _;

  root /usr/share/nginx/html;
  autoindex on;              # show file listing
  autoindex_exact_size off;
  autoindex_localtime on;

  location / {
    try_files $uri $uri/ =404;
  }
}

This gives me a simple webpage where I can browse the satellite images:

Meshtastic

Enable SPI on the Raspberry Pi for the SX1262 shield

I’m using the SPI0 and SPI1 interfaces on the Raspberry Pi 4. This page explains how to enable multiple SPI interfaces:

sudo nano /boot/firmware/config.txt

And add:

[all]
dtparam=spi=on
dtoverlay=spi0-cs
dtoverlay=spi1-1cs,cs0_pin=16

Now reboot and run pinctrl to confirm the pins are correctly setup:

Meshtastic docker container

First we’ll create files that tell Meshtastic which Raspberry Pi pins to use to communicate with the SPI modules. I store all my docker config files under /home/pi/docker. I will also be making a data folder that we will mount in the Meshtastic container, so that you don’t lose everything when restarting the container.

cd /home/pi/docker
mkdir meshtasticd0
cd meshtasticd0
mkdir data
nano config.yaml

In the config.yaml file, add:

Lora:
  Module: sx1262
  DIO2_AS_RF_SWITCH: true
  DIO3_TCXO_VOLTAGE: true
  #CS: 8
  IRQ: 22
  Busy: 23
  Reset: 24
  SX126X_ANT_SW: 25
  spidev: spidev0.0
  spiSpeed: 2000000

General:
  MACAddressSource: eth0

Webserver:
  Port: 9443
  RootPath: /usr/share/meshtasticd

Logging:
  LogLevel: debug

Note that the CS pin line is commented out, it’s not needed to set this here and will actually throw an error about it being in use. Now do the same steps but for meshtasticd1 (create a folder, create a data folder and a config.yaml file). This time, insert this:

Lora:
  Module: sx1262
  DIO2_AS_RF_SWITCH: true
  DIO3_TCXO_VOLTAGE: true
  #CS: 16
  IRQ: 6
  Busy: 12
  Reset: 13
  SX126X_ANT_SW: 26
  spidev: spidev1.0
  spiSpeed: 2000000

General:
  MACAddressSource: eth0

Webserver:
  Port: 9443
  RootPath: /usr/share/meshtasticd

Logging:
  LogLevel: debug

Once these folders and files are in place, it time to use docker-compose to start two Meshtastic Docker containers:

services:
    meshtasticd0:
        ports:
            - 4403:4403
            - 8443:9443
        restart: unless-stopped
        container_name: meshtasticd-5.8dBi
        image: meshtastic/meshtasticd:beta-debian
        privileged: true
        volumes:
          - /home/pi/docker/meshtasticd0/config.yaml:/etc/meshtasticd/config.yaml
          - /home/pi/docker/meshtasticd0/data:/var/lib/meshtasticd
          - /etc/localtime:/etc/localtime
        mac_address: "xx:xx:xx:xx:xx:xx"
    
    meshtasticd1:
        ports:
            - 4404:4403
            - 9443:9443
        restart: unless-stopped
        container_name: meshtasticd-10dBi
        image: meshtastic/meshtasticd:beta-debian
        privileged: true
        volumes:
          - /home/pi/docker/meshtasticd1/config.yaml:/etc/meshtasticd/config.yaml
          - /home/pi/docker/meshtasticd1/data:/var/lib/meshtasticd
          - /etc/localtime:/etc/localtime
        mac_address: "xx:xx:xx:xx:xx:xx"

Notice that I gave different ports to each container. In the Android Meshtastic app you will have to add this port when adding this node (<your ip>:4403 or 4404).

I experimented with only sharing the specific SPI interface to each container, but couldn’t get this working, so I simply added priviliged: true. That way they have access to all the devices on the host. Not the safest solution, but the one that works best in my situation.

The MAC address is something you need to add once the containers are running. Meshtastic defines its node ID based on this MAC address and in the config.yaml files we told it to get the MAC address from the eth0 interface of the container. Docker sometimes changes the MAC address of these interfaces on a reboot so make sure to set it, otherwise you might create a new node every time you restart the container! Best way to do this is to comment these two lines out initially (#), run the containers, see which MAC addresses they get and then add them in the docker-compose file.

And that’s it! Check the logs of the containers to see if you see anything weird pop up. If not, it’s now time to connect to them with the Android app and start configuring them!

Optional: announcing Meshtastic nodes to LAN

It’s easier when the new Meshtastic network nodes on the Raspberry Pi are automatically discovered in the Android app. For this we can announce the service using Avahi.

On the Raspberry Pi, which normally has an Avahi daemon running by default, go to /etc/avahi/services and do sudo nano meshtasticd0.service. Insert the following:

<?xml version="1.0" standalone="no"?><!--*-nxml-*-->
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
    <name>Meshtasticd0</name>
    <service protocol="ipv4">
        <type>_meshtastic._tcp</type>
        <port>4403</port>
    </service>
</service-group>

Now create a separate file for the meshtasticd1 node and simply change the port. Do sudo systemctl restart avahi-daemon and both nodes will now show up in the network tab in the Meshtastic app.

Optional 2: automatic updates of the Meshtastic nodes

Nobody likes updating their Meshtastic nodes. The absence of over-the-air updates (OTA) makes it a pain in the behind, climbing up to your node with a laptop and usb cable. Well, running Meshtastic in a Docker container, means you can use Watchtower to regularly check if a new image is available and automatically download and install it:

services:
    watchtower:
        image: containrrr/watchtower
        container_name: watchtower
        volumes:
            - /var/run/docker.sock:/var/run/docker.sock
        environment:
            - TZ=Europe/Brussels
            - WATCHTOWER_MONITOR_ONLY=false
            - WATCHTOWER_CLEANUP=true
            - WATCHTOWER_SCHEDULE= 0 0 5 * * 3
            - WATCHTOWER_NOTIFICATIONS=email
            - WATCHTOWER_NOTIFICATION_EMAIL_FROM=xxxxxxxxxxxxx
            - WATCHTOWER_NOTIFICATION_EMAIL_TO=xxxxxxxxxxxxxxxx
            - WATCHTOWER_NOTIFICATION_EMAIL_SERVER=xxxxxxxxxxxxxxxxx
            - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PASSWORD=xxxxxxxxxxxxx
            - WATCHTOWER_NOTIFICATION_EMAIL_SUBJECTTAG=AntenneMast Watchtower Alert - Container Updates
            - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_USER=xxxxxxxxxxxx
            - WATCHTOWER_NOTIFICATION_EMAIL_SERVER_PORT=xxx
        restart: unless-stopped

Every third weekday (Wednesday) at 5:00, Watchtower will do its thing, and having to manually update your nodes is a thing of the past.

Conclusion

We now have a Raspberry Pi, running two Meshtastic nodes, an SDR++ server and an autonomous Satdump. In the future I want to add ADS-B or AIS. It’s all accessible via a single ethernet cable. From anywhere in the house I can access it via wifi, or via vpn when I’m out of the house.

Niek

I'm a pharmacist who likes to travel, designing electrical stuff, HAM radio and sailing...

4 Responses

  1. Dave says:

    Will (impatiently) do.

    • Niek says:

      I ordered a small batch of PCBs yesterday, so I will be able to build about 10 boards. I will see how it goes and if there’s enough interest to build more.

  2. Dave says:

    Hi Niek,

    Awesome project and a write-up! You saved my sanity with the note about correlation between MAC and node ID – I almost gave up on mt’s docker because of multiple node IDs. Thank you!

    Got a question about the dual lora shield: how do I go about getting my hands on one?

    • Niek says:

      Hey Dave,
      I’m currently developing version 0.3, incorporating my lessons learned.
      Keep checking here for updates.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.