Building a TTN LoRaWAN gateway on Raspberry Pi OS Trixie

We wanted a stationary TTN gateway in the homelab. Pi 4, RAK9003 PoE HAT, RAK5146 SX1303 SPI EU868/GPS module, Raspberry Pi OS Trixie, Basics Station, EU TTN cluster. Standard parts, standard cluster, ought to be a half-day exercise.

It wasn't. .../and without "Schlaubischlumpf", the hardware might have ended in a drawer instead.

Every published guide we found targets Bookworm or earlier and the 32-bit Raspberry Pi OS image. Trixie plus the 64-bit image plus a vendor RAK5146 module that omits a sensor, Semtech's reference design assumes turns out to combine four independent ways for the build to fail before the gateway even talks to TTN. Here are the four traps, the one-line fixes, and what each one looks like when it bites.

The full runbook — every command, every exit checklist for all five phases, every citation — lives over at the operational runbook. This post is the headline.

Trap 1 — RAKWireless/sx1302_hal is a 404

That repository does not seem to exist publicly anymore. It looks like RAK does not maintain a public fork of the SX1303 HAL (anymore?). What we found instead is pinned copies of Semtech upstream embedded inside their own integration projects (rak_common_for_gateway, udp-packet-forwarder).

Fix: use Semtech upstream and patch the reset GPIOs to match the RAK Pi HAT's wiring

  • GPIO 17 for the SX1302 reset,
  • and GPIO 5 for the SX1261 reset, instead of the CoreCell defaults of 23 and 22):
git clone https://github.com/Lora-net/sx1302_hal.git
cd sx1302_hal
sed -i 's/^SX1302_RESET_PIN=.*/SX1302_RESET_PIN=17/' tools/reset_lgw.sh
sed -i 's/^SX1261_RESET_PIN=.*/SX1261_RESET_PIN=5/' tools/reset_lgw.sh

Trap 2 — sysfs GPIO is gone in Trixie, the reset script needs a pinctrl rewrite

The reset script Semtech ships in the standalone HAL still uses the legacy sysfs GPIO interface (/sys/class/gpio/export). That interface was removed from the Raspberry Pi kernel for Bookworm, and the removal carries forward into Trixie. First run produces:

cannot create /sys/class/gpio/gpio17/direction: Directory nonexistent

Fix: replace reset_lgw.sh with a pinctrl-based version. pinctrl ships with raspi-utils on Pi OS and has a critical property for this use case: it persists pin state across command invocations, because it writes directly to the GPIO controller registers. The SX1302's POWER_EN line has to stay high while chip_id does its SPI work, so libgpiod's gpioset — which releases lines when its process exits — would need --hold-time tricks that pinctrl doesn't.

The full script is in the runbook; the case-statement at the bottom is worth highlighting:

case "$1" in
    stop)
        iot_sk_term
        ;;
    *)
        iot_sk_reset
        ;;
esac

That permissive *) matters because chip_id calls the script as reset_lgw.sh start while Basics Station may call it with no argument or with the GPIO PIN as $1. A script written to recognise only start|stop fails Basics Station's --radio-init step with Usage: ... {start|stop} and exit code 1, and the radio never comes up.

Trap 3 — Our RAK5146 has no STTS751 temperature sensor

Semtech's SX1302 reference design wires an STTS751 temperature sensor on the internal I2C bus at one of three addresses (0x38 / 0x39 / 0x3B) for RSSI compensation. The RAK5146 datasheet does not list any temperature sensor — looks like the part is omitted from the module entirely. This seems to be acknowledged upstream — Issue #58 and PR #63 have been open without merging since the issue was first reported, as far as we can tell.

The standalone HAL (used by chip_id in our Phase C sanity check) returns a fatal LGW_HAL_ERROR from lgw_start() when no sensor is found. The vendored HAL inside Basics Station no longer has that fatal return — but the runtime receive path still seems to call stts751_get_temperature() roughly 50 times per second and treats each failed I2C read as lgw_receive error: -1, so in our build, the gateway looked online but did not actually deliver uplinks.

Fix: the Beyondlogic-style patch at loragw_stts751.c — make both functions return success unconditionally, with a fake 25 °C temperature for RSSI compensation:

F=deps/lgw1302/platform-corecell/libloragw/src/loragw_stts751.c
sed -i '/^int stts751_configure/a\    return LGW_I2C_SUCCESS;' $F
sed -i '/^int stts751_get_temperature/a\    return LGW_I2C_SUCCESS;' $F
sed -i '/^int stts751_get_temperature/a\    *temperature = 50.0f;' $F

The HAL now thinks a phantom sensor lives at I2C 0x39, the receive loop runs silently, and TTN sees clean uplinks.

Trap 4 — Basics Station's setup.gmk doesn't know about aarch64

64-bit Pi OS is now the recommended default. Basics Station's build, however, still seems to assume 32-bit ARM:

setup.gmk:60: No toolchain for platform 'corecell' and local arch is not 'arm-linux-gnueabihf'
make[4]: NO-TOOLCHAIN-FOUND-gcc: No such file or directory

Documented in Issue #163 and Issue #171; neither one had been merged when we wrote this.

Fix: one-line retarget of the platform's expected ARCH:

sed -i 's|ARCH.corecell *= *arm-linux-gnueabihf|ARCH.corecell = aarch64-linux-gnu|' setup.gmk
make platform=corecell variant=std

What goes right when the four traps are stepped over

Once those four fixes are in place, everything works exactly as it should:

[RAL:INFO] Concentrator started (2s352ms)
[S2E:INFO] Configuring for region: EU868 -- 863.0MHz..870.0MHz
[SYN:INFO] First PPS pulse acquired
[SYN:INFO] Obtained initial PPS offset (899917) - starting timesync with LNS
[SYN:INFO] Time ref: Last PPS sys->UTC=22:32:27.000000 SX130X->GPS=22:32:45.000000 leaps=18s diff=0
[S2E:INFO] Beaconing resumed - recovered GPS data: time

The SX1303 silicon is alive (v1.2, chip ID 0x12), Basics Station holds a clean WebSocket to eu1.cloud.thethings.network:8887, the EU868 channel plan comes down dynamically via TTN's router_config, and Class B beaconing is scheduled. TTN Console flips to Connected / Last seen seconds ago with status heartbeats every ~30s, and a Received indicator on the gateway dashboard.

Why bother with the GPS variant?

A reasonable question after seeing TTN Console refuse to auto-populate the gateway's location: if you'll enter coordinates manually anyway, what's the point of paying for GPS?

The point is timing, not location:

  • Class B beacons transmitted on a GPS-aligned 128-second schedule require GPS-disciplined PPS. Without it, no scheduled downlinks.
  • TDoA geolocation of devices that don't carry their own GPS works only when contributing gateways timestamp arrivals with sub-microsecond GPS precision — your gateway's timing is a network contribution, not just for you.
  • Frequency reference discipline keeps channel centres tighter under EU868, reducing crosstalk at the band edges.

For a stationary public-community gateway like this one, GPS earns its price specifically through the first two. The "no location" surprise in TTN Console is a red herring — fixed gateways set their location manually in TTN's General settings tab, regardless of whether they have GPS.

In summary

Trixie + 64-bit Pi OS + a vendor RAK5146 + Basics Station + TTN EU868 work together — but every step seems to have at least one upstream assumption that quietly broke since the last guide we could find was written. For our build, the four sed-and-pinctrl fixes above were the difference between "it doesn't even build" and "Connected, beaconing, time-synced".

If you're walking through this and want every checkpoint and citation, head over to the operational runbook.

Let's grab the ESP32 S3 with LoRaWAN support and do the pending hardware verification of the corresponding crates in datenkollektiv/rustyfarian-network.