prplOS Operations Skill
Hands-on field manual for configuring prplOS / prplmesh-based access points via serial console. Captures the TR-181 datamodel API surface, the daemon topology (wld ↔ beerocks ↔ hostapd), and the specific traps that bite anyone touching this firmware for the first time.
All requirements use RFC 2119 keywords (MUST, SHOULD, MAY).
Platform Topology — Know Before You Touch
┌───────────────────────────────┐
USB-Serial console │ /dev/cu.PL2303G-USBtoUART* │ 115200 8N1
└───────────────┬───────────────┘
│
BusyBox ash (root, no password by default)
│
┌─────────────────────┬─────────┼─────────────┬──────────────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
ubus / ubus-cli wld hostapd beerocks_* configure.sh
(JSON-RPC bus) (wireless (per-radio (prplmesh (/etc/script/root-ap/
driver hostapd controller + configure.sh —
TR-181 datamodel abstraction).conf agent) hardcoded SSIDs!)
exposed as generates ↑
WiFi.* / IP.* / runtime │
DNS.* / Routing.* config RUNS ONCE on AP
role provisioning,
writes its OWN
SSID/password
Key insight: the TR-181 datamodel is the API. ubus call <Object> _set writes parameters; wld regenerates /tmp/wlanX_hapd.conf and reloads hostapd. But beerocks_controller (EasyMesh) and configure.sh may push their own values on top, fighting your changes.
Console Connection
- MUST use 115200 baud, 8N1, no flow control. Other rates return garbage or silence.
- MUST install the Prolific PL2303 driver via
brew install --cask prolific-pl2303and approve the system extension in System Settings → General → Login Items & Extensions → Driver Extensions. State sequence on macOS Tahoe:[activated waiting for user]→ enable toggle →[activated enabled]→/dev/cu.PL2303G-USBtoUART*appears. - SHOULD drive the console programmatically with Python
pyserialinstead ofscreen, so output is captured and verifiable. Reference helper:
import serial, time
PORT = "/dev/cu.PL2303G-USBtoUART140" # name varies per device
def ap_run(cmds, idle=1.0, mw=20):
s = serial.Serial(PORT, 115200, timeout=0.2)
s.write(b"\r\n"); time.sleep(0.3)
while s.read(2048): pass # drain
for c in cmds:
s.write(c.encode() + b"\r\n")
# read until idle
end = time.time() + mw; last = time.time(); buf = b""
while time.time() < end:
chunk = s.read(1024)
if chunk: buf += chunk; last = time.time()
elif time.time() - last > idle: break
print(f"$ {c}\n{buf.decode('utf-8','replace')}")
s.close()
- MAY use
screen /dev/cu.PL2303G-USBtoUART* 115200for interactive sessions. Not recommended for automation — output cannot be captured cleanly. - MUST verify whoami/prompt before issuing commands (
root@prplOS:/#). prplOS reference firmware ships with no root password —# WARNING! There is no root password defined on this device!is normal but a security concern.
TR-181 / amx Datamodel Cheat Sheet
prplOS exposes a TR-181 datamodel (broadband forum standard) over ubus, named under Device.WiFi.*, Device.IP.*, etc. Two CLI flavors:
| Tool | Use For | Example |
|------|---------|---------|
| ubus call <Path> _set '{"parameters":{...}}' | Write parameters atomically | ubus call IP.Interface.2.IPv4Address.1 _set '{"parameters":{"IPAddress":"10.0.0.1"}}' |
| ubus call <Path> _add '{"parameters":{...}}' | Add a new instance to a multi-instance object | ubus call DNS.Client.Server _add '{"parameters":{"DNSServer":"8.8.8.8",...}}' |
| ubus-cli '<Path>.?' | Read everything under a path | ubus-cli 'WiFi.Radio.?' |
| ubus-cli '<Path>.<Param>?' | Read one parameter | ubus-cli 'WiFi.SSID.1.SSID?' |
| ubus -v list <Object> | List methods + their parameter schema | ubus -v list WiFi.Radio.1 |
- MUST check
amxd-error-codein the response.0= success.4= parameter not found / case mismatch / required field missing. - MUST NOT trust ubus-cli filter expressions like
[Alias=="..."]for verification — they often emitMissing or invalid operator. Use direct path queries instead. - SHOULD prefer
ubus -v list <Object>over guessing method names. The output reveals all_*and named methods with their schemas.
Standard Configuration Recipes
Static IP / Gateway / DNS
# 1. Static IP on WAN interface (interface index varies — check first with `ip a`)
ubus call IP.Interface.2.IPv4Address.1 _set '{"parameters":{
"AddressingType":"Static",
"IPAddress":"140.113.194.250",
"SubnetMask":"255.255.255.224"}}'
# 2. Default gateway
ubus call Routing.Router.1.IPv4Forwarding.1 _set '{"parameters":{
"GatewayIPAddress":"140.113.194.225",
"Enable":true,
"Origin":"Static"}}'
# 3. Device's own DNS resolver list — _add creates a new entry
ubus call DNS.Client.Server _add '{"parameters":{
"Enable":true,"DNSServer":"8.8.8.8","Type":"Static",
"Interface":"Device.IP.Interface.2"}}'
# 4. DNS served to clients (DNS.Relay.Forwarding) — Alias must be unique
ubus call DNS.Relay.Forwarding _add '{"parameters":{
"Enable":true,"DNSServer":"8.8.8.8","Type":"Static",
"Interface":"Device.Logical.Interface.1.","Alias":"upstream-1"}}'
# 5. Switch DNS Relay mode on the LAN bridge
ubus call DNS.Relay.X_PRPL-COM_Config.1 _set '{"parameters":{
"DNSMode":"Static","ForwardingRef":"8.8.8.8"}}'
- MUST verify with
ip -4 addr show wan,ip -4 route,cat /tmp/resolv.confafter applying. The TR-181 set returningerror-code: 0does not guarantee the kernel state is updated.
WiFi: Rename SSID + Set Passphrase
# SSID name lives on the SSID object
ubus call WiFi.SSID.1 _set '{"parameters":{"SSID":"WinLab"}}'
# Passphrase lives on AccessPoint.X.Security — note CASE on KeyPassPhrase!
ubus call WiFi.AccessPoint.1.Security _set '{"parameters":{
"KeyPassPhrase":"winlabisgood"}}'
# For WPA3 (mandatory on 6 GHz), ALSO set SAEPassphrase:
ubus call WiFi.AccessPoint.5.Security _set '{"parameters":{
"KeyPassPhrase":"winlabisgood","SAEPassphrase":"winlabisgood"}}'
- MUST spell the field as
KeyPassPhrasewith a capital P inPhrase. The variantKeyPassphrase(lowercase p) returnserror_code: 4 - required: true— silent confusion. - MUST also set
SAEPassphraseon any AccessPoint whoseSecurity.ModeEnabledisWPA3-Personal(or WPA2-WPA3 mixed). Setting onlyKeyPassPhrasewill not unlock SAE clients. - SHOULD check
Security.ModesAvailablebefore forcing WPA3 — 6 GHz radios typically only listWPA3-Personal; 2.4/5 GHz list multiple modes.
Channel Selection
# Atomically set channel + bandwidth + reason (preferred)
ubus call WiFi.Radio.1 setChanspec '{"channel":11,"bandwidth":"20MHz","reason":"manual"}'
ubus call WiFi.Radio.2 setChanspec '{"channel":149,"bandwidth":"80MHz","reason":"manual"}'
# Alternative for individual params (may not trigger driver reconfigure):
ubus call WiFi.Radio.1 _set '{"parameters":{"Channel":11,"OperatingChannelBandwidth":"20MHz"}}'
- MUST verify at both layers — TR-181 (
WiFi.Radio.X.Channel?) AND driver (iw dev | grep channel). Datamodel may report success while driver is still on the old channel during DFS lock. - SHOULD disable
AutoChannelEnable(set to0) before manual channel selection, otherwise ACS may override.
Spectrum Scan (find clean channels)
iw dev wlanX scan does NOT work while the radio is in AP mode (command failed: Network is down). Use the prplOS amx scan API instead:
# Trigger scan (async)
ubus call WiFi.Radio.1 startScan '{"forceFast":true}'
# Wait 12-18 seconds for completion
sleep 15
# Read results — minRssi filters weak signals
ubus call WiFi.Radio.1 getScanResults '{"minRssi":-95}'
- MUST wait at least 12 seconds after
startScanbefore callinggetScanResults. Querying earlier returns empty results. - SHOULD also use
iw dev wlanX survey dumpfor channel utilization (busy time / noise floor) — this works even in AP mode and gives complementary data: a channel with few visible APs but highchannel busy timeindicates non-WiFi interference (microwave, BT). - MAY call
WiFi.Radio.X scanCombinedDatafor richer per-AP info (beacon intervals, SignalNoiseRatio breakdowns).
The Traps — Read Before Debugging
Trap 1: configure.sh clobbers your SSID
/etc/script/root-ap/configure.sh contains hardcoded ubus-cli WiFi.SSID.X.SSID="TEST-BH" and KeyPassPhrase="prplmesh" lines. It does NOT auto-trigger (not in init.d / cron / hotplug), but anyone running service prplmesh gateway_mode will indirectly invoke it. After it runs, your manually-set values are gone.
- MUST patch
/etc/script/root-ap/configure.shitself if you want SSID/password to survive agateway_modecycle. Example:sed -i 's/TEST-BH/WinLab/g; s/TEST-FH/WinLab-Guest/g; \ s/KeyPassPhrase="prplmesh"/KeyPassPhrase="winlabisgood"/g; \ s/KeyPassPhrase="12345678"/KeyPassPhrase="winlabisgood"/g' \ /etc/script/root-ap/configure.sh - MUST also fix
MultiAPTypelines — see Trap 3.
Trap 2: Stopping prplmesh kills wld, removes the WiFi datamodel
/etc/init.d/prplmesh stop triggers prplmesh_whm stop, which kills wld as a side effect. After that, WiFi.* ubus calls return Command failed: Not found because there is no datamodel publisher.
- MUST NOT run
prplmesh stopbefore WiFi datamodel writes. Restart with/etc/init.d/prplmesh_whm start && sleep 5 && /etc/init.d/prplmesh startto bringwldback. - SHOULD prefer surgical
pkill -f beerocks_controllerif the goal is just to silence the EasyMesh push-back, rather than nukingwld.
Trap 3: MultiAPType="BackhaulBSS" blocks normal client association
configure.sh ships with main SSIDs (1, 4, 7) marked as BackhaulBSS and guest SSIDs (3, 6, 9) as FronthaulBSS. BackhaulBSS is meant for mesh agent-to-controller backhaul links and rejects normal client PSK associations. Symptom: client says "cannot join network" on main SSID but happily joins guest.
- MUST flip main APs to
FronthaulBSS(or empty"") for end-user use:ubus call WiFi.AccessPoint.1 _set '{"parameters":{"MultiAPType":"FronthaulBSS"}}' ubus call WiFi.AccessPoint.3 _set '{"parameters":{"MultiAPType":"FronthaulBSS"}}' ubus call WiFi.AccessPoint.5 _set '{"parameters":{"MultiAPType":"FronthaulBSS"}}' - MUST patch
configure.shaccordingly so the change persists acrossgateway_modecycles.
Trap 4: hostapd multi_ap value semantics (counter-intuitive)
In /tmp/wlanX_hapd.conf, the multi_ap=N field maps as:
| Value | Meaning |
|-------|---------|
| 0 | Disabled (plain AP) |
| 1 | Backhaul BSS |
| 2 | Fronthaul BSS ← what you want for normal clients |
| 3 | Both |
multi_ap=2 is the "normal client + EasyMesh fronthaul" mode. Do not panic when you see multi_ap=2 on your client-facing SSID — that is correct.
Trap 5: service prplmesh gateway_mode wipes default route + MASQUERADE rules
After the prplmesh gateway_mode provisioning runs, the device may end up with:
- No default route (
ip -4 routedoes not showdefault via ...) - No MASQUERADE rules in
iptables -t nat -L POSTROUTING(clients get DHCP but cannot reach the internet)
This produces the diagnostic signature: AP itself returns ping: connect: Network unreachable from ping 8.8.8.8.
- MUST re-add the default route via
ubus call Routing.Router.1.IPv4Forwarding.1 _set '{"parameters":{"GatewayIPAddress":"...","Enable":true,"Origin":"Static"}}'AND verify withip -4 route. If still missing, add directly withip route add default via <gw> dev wan. - MUST re-add MASQUERADE rules manually:
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o wan -j MASQUERADE iptables -t nat -A POSTROUTING -s 192.168.2.0/24 -o wan -j MASQUERADE iptables -t nat -A POSTROUTING -s 192.168.3.0/24 -o wan -j MASQUERADE - SHOULD prove the data path works by source-routed ping:
ping -c 3 -I 192.168.1.1 8.8.8.8. - SHOULD persist via the firewall datamodel (
Firewall.Service.NAT.*) — runtimeiptables -Adoes not survive reboot.
Trap 6: _scan method does not exist — use startScan + getScanResults
Common naming guesses (_scan, triggerScan, scan) are unreliable. The supported pair is:
startScan(async trigger, returns{"retval":""})getScanResults(read results from an internal cache)
iw dev wlanX scan is blocked while the radio is hosting an AP (command failed: Network is down).
Trap 7: phy ↔ netdev naming is non-obvious
The TR-181 datamodel may report WiFi.Radio.1.Name="wlan0" while iw dev wlan0 info shows it on a 5 GHz frequency. The Name field is unreliable for cross-referencing — always confirm with iw dev or survey dump to see the actual frequency.
Diagnostic Workflow (when something is wrong)
When a client reports "no internet" or "cannot join", proceed in this order:
- AP itself online?
ping -c 2 8.8.8.8from the AP. IfNetwork unreachable, fix routing/MASQUERADE first (Trap 5). - Default route present?
ip -4 route | grep default. Re-add via TR-181 if missing. - MASQUERADE present?
iptables -t nat -L POSTROUTING -nv | grep MASQUERADE. Re-add per-subnet rules if missing. - Client associates? Check
logread | grep -iE "deauth|disassoc|reject" | tail -20for the client MAC and rejection reason. - AccessPoint role correct?
ubus-cli 'WiFi.AccessPoint.[1-6].MultiAPType?'. FlipBackhaulBSS→FronthaulBSS(Trap 3). - hostapd config matches datamodel?
cat /tmp/wlanX_hapd.conf | grep -E '^ssid|^bss=|wpa_passphrase|multi_ap'. If datamodel saysWinLabbut hostapd saysTEST-BH, hostapd has not regenerated — re-run patchedconfigure.shor kick AP enable.
Useful One-Liners
# What's actually broadcasting right now
cat /tmp/wlan*_hapd.conf | grep -E '^ssid|wpa_passphrase|^bss=|^interface=|multi_ap'
# All SSIDs at a glance
ubus-cli 'WiFi.SSID.?' | grep -E 'SSID\.[0-9]+\.(SSID|Name|Status)='
# Connected clients with IPs
cat /tmp/dhcp.leases
# Beerocks (prplmesh) live decisions
tail -f /tmp/beerocks/logs/beerocks_controller.log
# Real radio state (channel, freq, bw)
iw dev | grep -E 'Interface|channel'
# Channel utilization (works in AP mode!)
iw dev wlan0 survey dump
# Firewall counters (zero counter = packet not reaching this chain)
iptables -L FORWARD -nv --line-numbers
iptables -t nat -L POSTROUTING -nv
# Process map
ps -ef | grep -iE 'beerocks|prplmesh|hostapd|wld|amx' | grep -v grep
Persistence Notes
- TR-181
_setwrites are typically persisted by the amx framework into per-service config under/etc/config/(e.g.,/etc/config/wld,/etc/config/tr181-*). Verify by checking the relevant config file after a write. - Direct
iptables -Aandip route addare runtime only — survive until reboot. For permanence write through TR-181 (Firewall.*,Routing.*) or the persistent uci config. /etc/script/root-ap/configure.shmodifications viased -isurvive reboot (it is in/etc/, not/tmp/), but a firmware upgrade will revert it.
When to Reach for This Skill
Trigger conditions:
- Working with any prplOS / prplmesh device — typically WiFi 7 reference platforms with
OperatingStandards=beon three radios (2.4 / 5 / 6 GHz). - Configuring an AP via console (USB-Serial → BusyBox → ubus).
- Debugging "client can't connect" / "client connects but no internet" / "SSID name keeps reverting" on a prplOS device.
- Reading or writing TR-181 / amx datamodel via ubus.
- Doing spectrum survey or channel selection on a busy environment.
When in doubt: dump the AccessPoint Security object first (ubus-cli 'WiFi.AccessPoint.X.Security.?') — that single command reveals available auth modes, current passphrase fields, and supported security profiles, which collapses most "why won't it connect" questions instantly.