Read everything a ScreenBeam / Actiontec MoCA adapter knows about your coax network — over plain HTTP, with no vendor tooling and no SSH.
A single static binary with no dependencies. Cross-compiles to anything Go targets, so it runs on the Pi, NAS or router that actually sits near your coax.
These adapters ship a small web UI that shows a fraction of what the hardware measures, and no usable CLI. Behind that UI is an undocumented JSON API. This tool speaks it directly: the full node table, the coordinator election, per-link PHY rates, the signal quality behind those rates, Ethernet negotiation, 64-bit frame counters, the band and channel plan, and the security configuration.
It also interprets what it reads, rather than just printing it — which link is degraded, whether that is attenuation or reflections, whether the fault is a node or the path between two nodes, and what will actually change if you retune.
$ moca-status -diagnose
WARN link-rate node 0 (Master Bedroom) <-> node 2 (Office) runs 1674/1706 Mbps,
well below the 3515 Mbps best link on this coax
INFO link-cause node 0 (Master Bedroom) <-> node 2 (Office) is limited by
attenuation rather than reflections - it carries 11386 bits per
symbol against a median of 20517 while its cyclic prefix is
normal, so look for signal loss on that run: a long cable, an
extra splitter, a corroded or loose fitting
Download a binary. Nothing to install — the binary is static and has no runtime.
# pick your platform from the latest release
curl -sSL https://github.com/kevinelliott/moca-status/releases/latest/download/moca-status-linux-arm64.tar.gz \
| tar xz && ./moca-status -versionOr run the container. Host networking matters: the adapters are on your LAN,
and -stress sends UDP that NAT would reshape.
docker run --rm --network host ghcr.io/kevinelliott/moca-status 192.168.1.10
# with a config file
docker run --rm --network host \
-v $PWD/moca_hosts.json:/moca_hosts.json:ro \
ghcr.io/kevinelliott/moca-status -diagnoseImages are published for linux/amd64, linux/arm64 and linux/arm/v7.
Or build it. Needs Go 1.21+ (1.25+ recommended; the toolchain is pinned in
mise.toml). Nothing else — there are no third-party dependencies.
git clone https://github.com/kevinelliott/moca-status.git
cd moca-status
make
# point it at your adapters
cp moca_hosts.example.json moca_hosts.json
$EDITOR moca_hosts.json
./moca-statusOr skip the config entirely and pass addresses on the command line:
./moca-status 192.168.1.10 192.168.1.11Flags take one dash or two — -diagnose and --diagnose both work.
make cross # linux/amd64, linux/arm64, linux/armv6, darwin/arm64, darwin/amd64Every adapter is queried concurrently, so a sweep costs about as long as the slowest single adapter rather than the sum of all of them.
The toolchain is pinned in mise.toml (Go 1.25.3). Any Go 1.21+ will build it,
but older toolchains on recent macOS need workarounds the Makefile no longer
carries: Go 1.21's internal linker omits LC_UUID, which recent dyld rejects,
and the externally-linked result then needs an ad-hoc signature. Go 1.25 emits a
linker-signed binary itself, so a plain go build just works.
Site-specific settings live in a config file so the source carries no particular network's addresses. It is searched for in this order:
$MOCA_CONFIG./moca_hosts.json~/.config/moca-status/hosts.json
{
"hosts": ["192.168.1.10", "192.168.1.11", "192.168.1.12"],
"expected": {
"aa:bb:cc:00:00:01": "Office",
"aa:bb:cc:00:00:02": "Living Room",
"aa:bb:cc:00:00:03": "Bedroom"
}
}expected is optional. It names adapters in the output, and lets the diagnostics tell you when an adapter you expect has vanished from the coax entirely — which is not otherwise obvious, since an adapter can drop off the bus while still appearing on your network.
Credentials default to the vendor's admin / screenbeam; override with -user / -password.
Everything the adapter knows about itself and the network.
$ ./moca-status 192.168.1.13
========================================================================
192.168.1.13 aa:bb:cc:00:00:03 "Living Room"
========================================================================
DEVICE
chip / SoC MXL371x.1.18.14 (chip id 0x16)
firmware words 1.18.14.1, 1.17.5.0
adapter name LivingRoom
gpio 0xfd
NETWORK INTERFACE
address 192.168.1.13 / 255.255.255.0 gw 192.168.1.1
DHCP false auto-IP false
ethernet port 0 down 10Mbps half
ethernet port 1 up 1Gbps full
MoCA
link UP
node id 3 NC = node 2
version node 2.5 network 2.5
beacon channel 1150 MHz primary LOF 1175 secondary LOF 1300
2.5 channels first 1175, count 5
preferred NC false network search true LOF 1150 MHz band 0x1
tx power 10 beacon power 10 hw cfg 0x80000040 scan mask 0x2
SECURITY
privacy disabled (mode mask 0x0)
privacy password 99999999988888888
enhanced privacy ENABLED (mode 0x7)
enhanced password 99999999988888888777
FRAME COUNTERS
tx good 306559 bad 0 dropped 0
rx good 131258 bad 0 dropped 0
COAX (bitmask 0xf -> 4 nodes)
node 0 aa:bb:cc:00:00:00 MoCA 2.5 Master Bedroom
node 1 aa:bb:cc:00:00:01 MoCA 2.5 Guest Room
node 2 aa:bb:cc:00:00:02 MoCA 2.5 Office [NC]
node 3 aa:bb:cc:00:00:03 MoCA 2.5 Living Room [self]
PHY RATES, Mbps (NPER; diagonal = GCD broadcast)
from\to 0 1 2 3
node 0 570 2838 1652 3527
node 1 2851 684 3446 3316
node 2 1677 3408 578 2496
node 3 3525 3269 2467 679
-brief drops the rate matrix. -vlper shows VLPER rates instead of NPER.
The rate alone does not tell you what to go and look at. The FMR payload carries two independent quantities, and they fail for different physical reasons:
| meaning | rises / falls with | |
|---|---|---|
ofdmb |
bits carried per OFDM symbol | falls with attenuation and poor SNR |
gap |
cyclic prefix length | rises with reflections and delay spread |
$ ./moca-status --signal
LINK SIGNAL QUALITY
========================================================================
ofdmb = bits per OFDM symbol, falls with attenuation
gap = cyclic prefix, rises with reflections and delay spread
network median: ofdmb 20506, gap 18
link gap ofdmb Mbps reading
Master Bedroom <-> Guest Room 20 19380 2871 normal
Master Bedroom <-> Office 20 11362 1672 attenuated
Master Bedroom <-> Living Room 20 23750 3516 normal
Guest Room <-> Office 14 22722 3414 normal
Guest Room <-> Living Room 12 21737 3298 normal
Office <-> Living Room 15 15969 2382 normal
A slow link with normal gap and low ofdmb is losing signal — a long run, an extra splitter, a corroded fitting. A slow link with normal ofdmb and high gap is seeing reflections — a stub or an unterminated leg. Different symptom, different repair.
Reasons across every adapter at once, so it catches what no single readout shows.
$ ./moca-status --diagnose
DIAGNOSTICS
========================================================================
WARN: 1 INFO: 3
--- WARN ---
WARN link-rate node 0 (Master Bedroom) <-> node 2 (Office) runs 1674/1706 Mbps,
well below the 3515 Mbps best link on this coax
--- INFO ---
INFO link-cause node 0 (Master Bedroom) <-> node 2 (Office) is limited by
attenuation rather than reflections - it carries 11386 bits per
symbol against a median of 20517 while its cyclic prefix is
normal, so look for signal loss on that run: a long cable, an
extra splitter, a corroded or loose fitting
INFO bottleneck 192.168.1.12 negotiates 1000 Mbps on Ethernet but MoCA 2.5
carries up to 2500 Mbps here - the Ethernet port is the limit
INFO bottleneck 192.168.1.13 negotiates 1000 Mbps on Ethernet but MoCA 2.5
carries up to 2500 Mbps here - the Ethernet port is the limit
Add -v to print the full readout as well.
All checks
| category | catches |
|---|---|
reachability |
an adapter that stopped answering |
membership |
an expected adapter absent from the coax; a node bridging but not directly queryable |
consistency |
adapters disagreeing on the node bitmask or the coordinator; mismatched LOF, band mask, tx power, beacon power |
coordinator |
no Preferred NC set, two contending, or one set that has not taken over |
moca-version |
a pre-2.0 node forcing the network into mixed mode |
link-rate |
links degraded in absolute terms and relative to the best link on the bus |
link-cause |
whether a slow link is attenuation, reflections, or both |
node-local |
a node slow to every peer (its own drop) versus one slow pair (the path) |
link-asymmetry |
directional impairment, which points at a splitter leg rather than plain loss |
broadcast |
a low GCD rate, which multicast and MoCA control traffic ride |
frame-errors |
bad and dropped frames as ppm of good |
ethernet |
port down, half duplex |
bottleneck |
an Ethernet port slower than the MoCA generation's rated throughput |
security |
privacy settings that differ between adapters |
config |
DHCP on a management interface, network search switched off |
$ ./moca-status --watch --interval 30
[18:43:15] watching 4 adapter(s) every 30s - Ctrl-C to stop
[18:43:37] baseline: 4/4 reachable, 4 node(s) on coax, NC=node 2, 2 finding(s)
[18:51:02] ALERT node 3 (Living Room) left the coax
[18:51:02] NEW CRIT membership Living Room (aa:bb:cc:00:00:03) is not on the coax
[18:53:30] ALERT node 3 (Living Room) joined the coax
[18:53:30] CLEARED CRIT membership Living Room (aa:bb:cc:00:00:03) is not on the coax
[19:04:11] ERROR 192.168.1.12 rx bad +37 in 30s (total 37)
[19:12:40] ALERT 192.168.1.11 tx counters reset (3052298 -> 1204) - the adapter rebooted
Reports nodes joining or leaving, the coordinator moving, link and config changes, error counters incrementing with their delta, counters resetting (an adapter rebooted), link rates moving more than 15%, and findings appearing or clearing.
Real PHY rates jitter about 1% between polls and the good-frame counters climb constantly, so both are ignored by design — a stable network prints nothing after the baseline line. That is asserted by the test suite, not assumed.
-count N stops after N polls, which makes it usable from cron.
The plan is not assumed. Each band carries a 64-bit scan mask and an offset, and freq = (63 - bit + offset) * 25 MHz. That decode reproduces the published MoCA band plans exactly, which is how it was verified: band E resolves to 500–600 MHz, band F to 675–850 MHz, D-High to 1400–1600 MHz.
$ ./moca-status --plan
CHANNEL PLAN
========================================================================
hwCfgWord 0x80000040 band mask 0x1 current LOF 1150 MHz
band hw on channels (MHz)
D-Ext yes yes 1150, 1200, 1250, 1300, 1350, 1400, 1450, 1500, 1550, 1600
D-Low no no 1150, 1200
D-High no no 1400, 1450, 1500, 1550, 1600
E no no 500, 525, 550, 575, 600
F-SAT no no 675, 700, 725, 750, 775, 800, 825, 850
F-CBL no no 675, 700, 725, 750, 775, 800, 825, 850
H no no 975, 1000, 1025
- coax profile: none - dedicated MoCA coax - the whole band is available
- bands this hardware supports: D-Ext
- only one band is supported, so there is no alternative band to move
to; tuning here means choosing a channel within it
- coax loss rises with frequency, so on a dedicated line the lowest
channel is the best default: 1150 MHz
- current LOF 1150 MHz is already the best available choice
hwCfgWord bit 31 - bandIdx says whether the hardware supports a band at all, so the tool will tell you plainly when there is nothing to retune.
If something else shares your coax, say so and the advice changes:
$ ./moca-status --plan --shared-coax docsis31
- coax profile: docsis31 - DOCSIS 3.1 downstream reaches 1218 MHz
- channels overlapping docsis31: 1150, 1200, 1250
- lowest channel clear of docsis31: 1300 MHz (fit a point-of-entry
filter as well, so MoCA does not leave the house)
- current LOF 1150 MHz OVERLAPS docsis31 - move to 1300 MHz
| profile | ceiling | |
|---|---|---|
none |
— | dedicated MoCA coax (default) |
docsis30 |
1002 MHz | DOCSIS 3.0 downstream |
docsis31 |
1218 MHz | DOCSIS 3.1 downstream |
docsis40 |
1794 MHz | DOCSIS 4.0 extended spectrum |
satellite |
2150 MHz | DirecTV / SWM |
A MoCA channel centred at f occupies roughly f ± 50 MHz, so it is clear only when f − 50 sits above the ceiling. When nothing clears, the tool says so rather than inventing a recommendation.
Marginal coax passes an idle check and only misbehaves when busy, either by incrementing error counters or by backing the modulation off. Neither shows up in a static readout.
$ ./moca-status --stress --target 192.168.1.50 --stress-seconds 25 --stress-mbps 800
sent 1606848 datagrams, 17997 Mbit in 25s (720 Mbit/s offered)
adapter frames tx frames rx errors
192.168.1.10 243311 96452 0
192.168.1.11 34282 10327 0
192.168.1.12 29474 213835 0
192.168.1.13 33171 25416 0
link before after change
node 0 -> node 2 1671 1654 -1.0%
node 1 -> node 2 3438 3424 -0.4%
RESULT: no frame errors and no meaningful rate change - the coax handled the load.
-target should be a host behind a MoCA-attached switch. UDP is used, so no listener is needed — the frames still have to cross the coax to reach it. The tool warns if the counters barely move, meaning your target was not actually across the link.
A remote adapter's own management interface sits on the far side of the coax, so traffic addressed to it must cross the link being tested.
$ ./moca-status --stress --via-adapters --stress-seconds 12 --stress-mbps 200
coordinator (head end): 192.168.1.10 - traffic to it never crosses the coax, skipped
adapter link sent crossed errors rate chg
192.168.1.11 node 2 -> 1 207060 38456 0 +0.3%
192.168.1.12 node 2 -> 0 208845 57953 0 -0.2%
192.168.1.13 node 2 -> 3 207060 40986 0 +0.4%
The limitation is real and worth understanding. Those frames terminate on the adapter's small CPU instead of passing through its bridge in hardware, so it drops most of them — measured, about 28% arrived, against 88% when the target was a real host on the far side. Use -via-adapters to prove a link is clean; use -target to measure what it can carry.
Everything above, machine-readable. With -diagnose, findings are included and each carries a stable key identifying its subject, so you can diff runs without matching on prose.
{
"adapters": [ { "ip": "192.168.1.10", "moca_link": "up", "...": "..." } ],
"findings": [
{ "severity": "WARN", "category": "link-rate", "key": "link:0-2",
"message": "node 0 (Master Bedroom) <-> node 2 (Office) runs 1674/1706 Mbps ..." }
]
}Reverse-engineered from the adapters' own UI (index / devStatus / devSetup / security / phyRates .html + main.js).
GET http://<ip>/phyRates.html -> sets the csrf_token cookie (Basic auth)
POST http://<ip>/ms/<tree>/<hexid>
headers: X-CSRF-TOKEN: <csrf_token cookie>
Content-Type: application/x-www-form-urlencoded
body: {"data":[...]} <- JSON, despite that content type
reply: {"data":["0x..","0x.."]} <- 32-bit words as hex strings
GET http://<ip>/ms/<tree>/<hexid>/GET -> same shape, for config reads
The id may be hex with 0x or plain decimal — the vendor's own UI uses both.
| endpoint | body | returns |
|---|---|---|
/ms/0/0x14 |
[0] |
frame counters, 64-bit as hi<<32|lo: [12] tx good, [30] tx bad, [48] tx dropped, [66] rx good, [84] rx bad, [102] rx dropped |
/ms/0/0x15 |
[] |
[0] node id, [1] NC node id, [5] MoCA link, [11] network version, [12] node bitmask, [18]/[19] LOF offsets, [21..] SoC version ASCII, [28] band gap mode |
/ms/0/0x16 |
[node] |
[0..1] MAC, [4]&0xff MoCA version |
/ms/0/0x1D |
[1<<node, ver] |
FMR payload; PHY data from word 10 |
/ms/0/0x24 |
[] |
beacon channel |
/ms/0/0x7f |
[] |
[2] first channel, [3] channel count |
/ms/1/0xb17 |
[] |
gpio |
Config reads (/GET): 0x1001 network search · 0x1002 preferred NC · 0x1003 LOF · 0x1009 band mask · 0x100e beacon power · 0x10a8 tx power · 0x10f5 hw config word · 0x1049 privacy password · 0x1059 privacy mode mask · 0x130D enhanced privacy · 0x1400 enhanced password · 0x1063+band scan mask · 0x106b+band scan offset
Device tree (/ms/1/, /GET): 0x103 MAC · 0x20b IP · 0x210 netmask · 0x211 gateway · 0x212 adapter name index · 0x217 DHCP · 0x218 auto-IP · 0x302 firmware · 0x303 chip id · 0x307 Ethernet link/speed/duplex
The node bitmask is the authoritative test of whether an adapter is really on the coax. It comes from the MoCA layer itself, so it stays correct even when the adapter has no IP and is invisible to ARP, DHCP, packet capture and your controller — which is exactly the state a bridging adapter with no management address is in.
The adapters run a single-threaded InterNiche webserver that intermittently answers 401 or drops the connection when requests land close together, and a full readout makes about twenty requests per adapter. The client retries rather than treating that as fatal — an unretried transient 401 reads as "adapter absent", which is wrong and alarming.
Port 23 is open on these adapters and drops straight to a > prompt with no authentication at all, exposing reg_wr / mdio_wr / smmd_wr — raw writes that can brick the device until a power cycle. It offers nothing useful in return: every read on the MDIO bus returns 0xffff, meaning nothing is attached to it. There is no setting to disable it, so the only control is network placement. This tool does not use it.
make test51 checks, no hardware required. Every diagnostic is driven against the fault it is meant to catch, and — just as importantly — a healthy network and a network whose numbers merely drift must both stay silent. That second half is what catches the interesting bugs: it is how an earlier bottleneck check was found to be comparing Ethernet speed against the raw PHY rate rather than the generation's rated throughput, which flagged even a correctly matched 2.5 Gb port.
Run it after changing any threshold in diagnose.go (RateCrit, RateWarn, AsymmetryWarn, ErrorPPMCrit, PhyDrift).
Developed against ScreenBeam ECB6250 (MaxLinear MXL371x, SoC 1.18.14) running MoCA 2.5. Other MaxLinear-based Actiontec/ScreenBeam adapters share the same /ms/ API and are likely to work; the tool reads the chip id and band support from the hardware rather than assuming them.
If you try it on other hardware, an issue with the output of -plan and the full readout would be welcome.
MIT