RAKPiOS Tools
Beyond rakpios-cli and mioty-cli, which have pages of their own, RAKPiOS ships a handful of utilities and background services that make the gateway hardware work and keep it observable. This page covers them.
Raspberry Pi raspi-config Tool
RAKPiOS includes raspi-config, the official Raspberry Pi OS configuration utility, for system settings, interface settings, localisation, and performance options.
Figure 1: raspi-config toolRAKPiOS has already applied the settings a headless gateway needs, so there is normally no reason to touch them:
| Setting | State in RAKPiOS |
|---|---|
| SSH | Enabled |
| I2C | Enabled |
| SPI | Enabled |
| WiFi country | GB |
- Set your own WiFi country. RAKPiOS ships with the country set to
GBso that the WiFi radio comes up at all. It governs which channels and power levels are legal, so change it to where the gateway is actually deployed, under Localisation Options → WLAN Country. - Do not configure networking with
raspi-config. RAKPiOS manages every interface with NetworkManager, andrakpios-clidrives it. Refer to the Command Line Utility page instead.
OLED Status Panel
RAKPiOS displays system information on an SSD1306 OLED screen when one is fitted, as on the WisGate Connect. The panel is a small native program at /usr/local/bin/oled, run as a systemd service, and its source is on GitHub.
It drives a 128×64 monochrome SSD1306 over I2C, at address 0x3C on /dev/i2c-1.
Pages
The display cycles through its pages every five seconds:
| Page | Shows |
|---|---|
| Intro | The OS name and version, from /etc/os-release. Shown once at startup. |
| Network | The active IPv4 addresses. Only interfaces with a default route are listed, so Docker bridges and internal module interfaces stay out of the way. |
| Docker | The names of running containers, four per page. Skipped when nothing is running. |
| Stats | CPU usage, free memory, temperature, and uptime. |
Power Supply Warnings
On the RAK7391, the power supply monitor is wired to GPIO 16. When it asserts, the panel stops cycling and shows a POWER SUPPLY ISSUES ! warning for 60 seconds before resuming.
The pin is not read directly. It is handed to the kernel's gpio-keys driver, which republishes it as an input event device reporting key code 148 (KEY_PROG1). A GPIO character device line can only be claimed by one process at a time. Whichever service asked first would therefore lock every other service out. Using gpio-keys allows both the OLED panel and the hardware test suite to watch the same pin. Events are also still delivered when the panel is not running.
RAKPiOS wires this up automatically as part of board detection; it applies to the RAK7391 only, because it is the only carrier that routes the signal.
Because the kernel owns the line, your own code can watch the same signal at the same time as the OLED panel. Refer to Monitoring the Power Supply Signal.
Controlling the Service
# Check what it is doing
rak@rakpios:~ $ systemctl status oled
# Stop it for now
rak@rakpios:~ $ sudo systemctl stop oled
# Stop it, and keep it off across reboots
rak@rakpios:~ $ sudo systemctl disable --now oled
# Turn it back on
rak@rakpios:~ $ sudo systemctl enable --now oled
Changing What Is Displayed
The panel takes command-line options:
| Option | Effect |
|---|---|
--no-intro | Skip the startup splash page |
--no-network | Disable the network page |
--no-docker | Disable the Docker page |
--no-stats | Disable the system stats page |
--all-ifaces | List every IPv4 interface, not only those with a default route |
--version | Print the version and exit |
--help | Show usage and exit |
Disabling all four pages is rejected.
To make an option stick, override the service unit:
rak@rakpios:~ $ sudo systemctl edit oled
and add:
[Service]
ExecStart=
ExecStart=/usr/local/bin/oled --all-ifaces
The empty ExecStart= is required. It clears the original line before setting the new one. Then reload and restart:
rak@rakpios:~ $ sudo systemctl daemon-reload && sudo systemctl restart oled
Monitoring the Power Supply Signal
On the RAK7391, the power supply monitor is wired to GPIO 16 and published by the kernel's gpio-keys driver as key code 148 (KEY_PROG1) on an input event device. You can watch it from your own code to log brownouts, publish them over MQTT, shut a service down cleanly, or trigger a graceful power-off.
Doing it this way rather than claiming the GPIO directly is what makes that possible: a GPIO character device line belongs to one process at a time, so a program holding line 16 would lock everyone else out, including the OLED panel. An input event node can be opened by any number of readers at once, and each gets its own copy of every event.
| Event Attribute | Description |
|---|---|
| Event type | EV_KEY (1) |
| Key code | 148 (KEY_PROG1) |
| Value 1 | Power fault asserted: the pin is active low, so this is a falling edge |
| Value 0 | Power fault cleared |
This applies to the RAK7391 only, because it is the only carrier that routes the signal, and only once the gpio-key overlay is in place. RAKPiOS applies it automatically during board detection.
If nothing reports key code 148, check that the overlay is present:
rak@rakpios:~ $ grep gpio-key /boot/firmware/config.txt
dtoverlay=gpio-key,gpio=16,active_low=1,gpio_pull=up,label=power-fault,keycode=148
Finding the Input Device
The device number is not fixed, and you cannot find the device by name. The overlay applies its label= to the key node rather than to the parent gpio-keys node, so the input device ends up with a generic name. Look for the key code it advertises instead.
evtest lists every input device with the codes it reports:
rak@rakpios:~ $ sudo apt install evtest
rak@rakpios:~ $ sudo evtest
No device specified, trying to scan all of /dev/input/event*
Available devices:
/dev/input/event0: gpio-keys
Select the device event number [0-0]: 0
Watching It Live
Once attached, evtest prints each transition as it happens. Simulate or wait for a fault and you will see the pin assert and release:
Testing ... (interrupt to exit)
Event: time 1755500000.123456, type 1 (EV_KEY), code 148 (KEY_PROG1), value 1
Event: time 1755500000.123456, -------------- SYN_REPORT ------------
Event: time 1755500004.654321, type 1 (EV_KEY), code 148 (KEY_PROG1), value 0
Event: time 1755500004.654321, -------------- SYN_REPORT ------------
To ask whether a fault is active right now, rather than waiting for the next change, use a one-shot query. It exits with status 10 while the key is held down and 0 otherwise:
rak@rakpios:~ $ sudo evtest --query /dev/input/event0 EV_KEY 148
rak@rakpios:~ $ echo $?
0
Reacting to It From a Script
The event node is a stream of fixed-size input_event structures, so no library is needed. This script finds the right device by capability, then blocks until something happens:
#!/usr/bin/env python3
"""Watch the RAK7391 power supply monitor and react to power faults."""
import glob
import os
import struct
import sys
import time
KEYCODE = 148 # KEY_PROG1, as set by the gpio-key overlay
EV_KEY = 1
# struct input_event on a 64-bit kernel: a 16-byte timeval, then the type,
# code and value. RAKPiOS is arm64 throughout, so this layout always holds.
EVENT_FORMAT = "llHHi"
EVENT_SIZE = struct.calcsize(EVENT_FORMAT) # 24 bytes
def find_device(keycode=KEYCODE):
"""Return the /dev/input node of the device reporting this key code.
The device cannot be looked up by name, so match on the capability
bitmap in sysfs. Where more than one device reports the code, prefer the
one advertising the fewest codes overall: that is the single-button GPIO
device rather than, say, an attached USB keyboard.
"""
word, bit = divmod(keycode, 64) # 64-bit words on arm64
best = None
for path in sorted(glob.glob("/sys/class/input/input*")):
try:
with open(os.path.join(path, "capabilities", "key")) as f:
# Printed most significant word first, leading zero words
# omitted, so reverse to index by word number.
words = [int(w, 16) for w in f.read().split()][::-1]
except OSError:
continue
if word >= len(words) or not (words[word] >> bit) & 1:
continue
nodes = glob.glob(os.path.join(path, "event*"))
if not nodes:
continue
total = sum(bin(w).count("1") for w in words)
if best is None or total < best[0]:
best = (total, "/dev/input/" + os.path.basename(nodes[0]))
return best[1] if best else None
def on_power_fault(active):
"""Called on every transition. Replace with whatever you need to do."""
state = "ASSERTED" if active else "cleared"
print(f"{time.strftime('%Y-%m-%d %H:%M:%S')} power fault {state}", flush=True)
def main():
device = find_device()
if device is None:
sys.exit(f"no input device reports key code {KEYCODE} - "
"is the gpio-key overlay applied?")
print(f"watching {device} for key code {KEYCODE}", flush=True)
# Unbuffered, so each read returns as soon as one event is available
# instead of waiting for a buffer to fill.
with open(device, "rb", buffering=0) as f:
while True:
data = f.read(EVENT_SIZE)
if not data:
break
_sec, _usec, etype, code, value = struct.unpack(EVENT_FORMAT, data)
# Value 2 is autorepeat, which is not a state change.
if etype == EV_KEY and code == KEYCODE and value in (0, 1):
on_power_fault(value == 1)
if __name__ == "__main__":
main()
Run it:
rak@rakpios:~ $ sudo python3 power-fault-monitor.py
watching /dev/input/event0 for key code 148
2026-08-18 11:04:12 power fault ASSERTED
2026-08-18 11:04:16 power fault cleared
Reading /dev/input/event* needs root, or membership of the input group. To run the monitor as an ordinary user:
rak@rakpios:~ $ sudo usermod -aG input $USER
Log out and back in for the new group to take effect.
Running It as a Service
Install the script and give it a unit so it starts with the gateway:
rak@rakpios:~ $ sudo install -m 755 power-fault-monitor.py /usr/local/bin/power-fault-monitor
rak@rakpios:~ $ sudo systemctl edit --force --full power-fault-monitor.service
[Unit]
Description=RAK7391 power supply monitor
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/local/bin/power-fault-monitor
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
rak@rakpios:~ $ sudo systemctl enable --now power-fault-monitor
rak@rakpios:~ $ journalctl -u power-fault-monitor -f
Because the kernel owns the GPIO line, this service and the OLED panel both see every event, and neither prevents the other from running.
Board Detection
RAKPiOS is a single image for several carrier boards, which do not share a device tree. The RAK7391 has two GPIO expanders; the RAK7392 and RAK7393 need a UART for GPS; the RAK7392 needs a different SPI chip-select layout because GPIO 7 is wired to the WisBlock reset line there. All of them take a CM4 or CM5, so the [cm4] and [cm5] conditionals in config.txt cannot tell them apart.
The rak-board-detect service runs at boot, works out which carrier it is on, and writes the matching overlay lines into /boot/firmware/config.txt, between two marker comments:
# BEGIN RAK BOARD
# Managed by rak-board-detect - put a board name in /boot/firmware/rak-board to override.
# Keep both markers, and keep them in an [all] section.
# Detected carrier board: rak7391
dtoverlay=rak7391
dtoverlay=gpio-key,gpio=16,active_low=1,gpio_pull=up,label=power-fault,keycode=148
# END RAK BOARD
| Board | Detected by | Overlays applied |
|---|---|---|
| RAK7391 | Its two PCA9555 GPIO expanders on I2C | rak7391 (names the expander lines), plus the GPIO 16 power fault key |
| RAK7392 | Its RTL8111H Ethernet controllers, or four or more Ethernet ports | spi0-1cs, uart5 |
| RAK7393 | Neither of the above | uart5 |
The gateway reboots once by itself after the first boot from a fresh image. Overlays only load at boot, so the service reboots to apply what it just wrote. It only does this when the block actually changes, and it will not reboot more than three times in a row, so a detection that somehow fails to settle cannot loop forever.
The same mechanism means an SD card moved from one carrier to another re-detects and fixes itself on the next boot.
Running It by Hand
# Report the detected board and apply it
rak@rakpios:~ $ sudo rak-board-detect
# Force a board instead of detecting
rak@rakpios:~ $ sudo rak-board-detect rak7392
Neither form reboots; run sudo reboot yourself for the overlays to take effect.
Overriding Detection Permanently
Put a board name in /boot/firmware/rak-board and it wins over detection on every boot:
rak@rakpios:~ $ echo rak7392 | sudo tee /boot/firmware/rak-board
The file is on the boot partition, so you can also write it from the computer you flashed the card with, before ever powering the gateway on. Delete it to go back to automatic detection.
WiFi Access Point Fallback
On gateways with WiFi, if RAKPiOS boots and finds no active connection, it brings up an access point with a captive portal so that you can configure the real network from a phone or a laptop. It is built on WiFi Connect by Balena.
| SSID | RAK_XXXX, where XXXX is the last four hexadecimal digits of the eth0 MAC address |
| Password | rakwireless |
| Portal address | 192.168.230.1 |
Connect to the access point and the captive portal should open by itself. If it does not, browse to 192.168.230.1. Pick your network, enter its password, and the gateway joins it.
The check runs once at boot: if any connection other than the loopback or a bridge is already active, the access point is skipped. So the fallback disappears as soon as the gateway has a real network, and comes back if it ever boots without one.
To change the SSID or password, override the service:
rak@rakpios:~ $ sudo systemctl edit create-ap
[Service]
Environment=SSID=MyGateway
Environment=PASS=mypassword
To disable the fallback entirely:
rak@rakpios:~ $ sudo systemctl disable --now create-ap
WisBlock Serial Ports
WisBlock modules attached to the gateway appear behind a USB hub as CH340 serial bridges. RAKPiOS gives them stable names so you do not have to guess which /dev/ttyUSB* is which:
rak@rakpios:~ $ ls -l /dev/ttyWB*
Each WisBlock slot gets its own /dev/ttyWB<N> symlink, which stays the same across reboots.
These ports are also hidden from ModemManager. ModemManager probes every serial device that appears, holding it open and writing AT commands at it for a few seconds, which corrupts whatever is already talking to the WisBlock. The rule is narrow enough that a real cellular modem is still detected normally.
Cellular Modems
ModemManager is installed and NetworkManager drives it, so a modem in a mini PCIe slot is configured the same way as any other connection. Refer to the Command Line Utility page for instructions on entering the APN and SIM PIN.
Quectel BG96 modules need their wwan0 interface put into raw IP mode before it will carry traffic. RAKPiOS does that automatically with a NetworkManager dispatcher hook, so no manual step is required.
Password Reset
reset_password returns the current user's login to the factory state: the password goes back to changeme, and it is marked expired so that the next login forces a new one to be set.
rak@rakpios:~ $ reset_password
This is for handing a gateway on to someone else, or putting a unit back to a known state, not for recovering a lost password. The utility uses sudo, so it asks for the password you are trying to replace.
If the password is genuinely lost, the way back in is to mount the boot partition on another computer and use the standard Raspberry Pi OS recovery procedure, or to reflash the image.
Hardware Test Suite
RAKPiOS ships a test suite that exercises every peripheral on the board, such as LEDs, buzzer, GPIO expanders, WiFi, fan driver, OLED, ADC, RTC, security element, temperature sensor, concentrators, eMMC, USB ports, and Ethernet controllers. It is useful for acceptance-testing a unit and for narrowing down which part of a gateway is at fault.
It is cloned into the default user's home directory:
rak@rakpios:~ $ cd ~/.local/share/rak739x-hardware-test
rak@rakpios:~ $ ./run.sh
Run with no arguments, it lists the configurations it knows about. Each one is a particular board with a particular set of radios fitted, and selects the subset of tests that apply:
Usage: ./run.sh <configuration_id>
Posible configuration_id values:
* rak7391-indoor-lora
* rak7391-indoor-lora-2g4
* rak7391-indoor-lora-lte
* rak7391-indoor-lora-mioty
* rak7391-indoor-lora-mioty-lte
* rak7391-outdoor-lora
* rak7391-outdoor-lora-lte
* rak7391-outdoor-lora-mioty
* rak7391-outdoor-lora-mioty-lte
* rak7392-lora
* rak7392-lte
* rak7392-mioty
* rak7393-lora-lte
* rak7393-lora-16ch-lte
* rak7393-lora-2g4-lte
* rak7393-lora-mioty-lte
* rak7394-lora
Pick the one that matches your unit:
rak@rakpios:~ $ ./run.sh rak7391-indoor-lora-mioty-lte
The run prints the board identity first, then each test as it goes, and a summary at the end:
CPU: Raspberry Pi Compute Module 4 Rev 1.1
CPU Serial Number: 10000000dfxxxxxx
Memory: 3.7Gi
Storage: 29G
Device EUI: d83addFFFExxxxxx
OS: rakpios-1.0.0-arm64
testLED
testBuzzer
testGPIOExpanders
...
testRTL8125
Ran 19 tests.
OK
A failure names the test and what it expected, which usually identifies the missing or faulty part directly:
testRAK5148
ASSERT:Wrong number of RAK5148 found expected:<1> but was:<0>
Ran 18 tests.
FAILED (failures=1)
The suite is on GitHub. It installs its Python dependencies into a virtual environment when it starts and removes them when it finishes, so it leaves nothing behind.
Message of the Day
Logging in over SSH prints a summary of the gateway rather than the stock Debian banner:
- The hostname, as a banner
- Distribution and kernel version, uptime, load averages, and process counts
- CPU model and core count, memory use, and CPU temperature
- The active IP addresses, with Docker bridges filtered out
- Disk space
- Running containers
The scripts behind it are in /etc/update-motd.d/, and behave like any other MOTD fragment: delete one to drop that section, or add your own.
Miromico Miro EdgeCard mioty® Management
Refer to the Miromico Miro EdgeCard mioty Management page.
