checking the mesh¶
how i check the two border routers are one mesh, what each one publishes, and
what a failover looks like. the examples use my ESP's name, esp-ot-br-33f0,
which comes from step 4 of
build and flash it.
the ESP's API¶
it returns one entry per router, with its extended address, Rloc16 and links.
the router ID is Rloc16 divided by 1024. a child's Rloc16 has a non zero
remainder. LeaderData.PartitionId should be the same in every entry.
mDNS¶
every border router announces _meshcop._udp:
dns-sd is macOS. on Linux the same two are avahi-browse _meshcop._udp and
avahi-browse -r _meshcop._udp. most TXT values are binary, so both print them
garbled.
| key | what it is |
|---|---|
nn |
network name |
xp |
extended PAN ID. Home Assistant groups border routers into a network by this |
pt |
partition ID, a random number picked by the leader and only announced once attached. the same pt on two border routers means one mesh |
sb |
state bitmap. bits 0 to 2 connection mode, 3 to 4 Thread interface (2 is active), 7 backbone router active, 8 backbone router primary, 9 to 10 role (1 child, 2 router, 3 leader), 11 ePSKc supported |
omr |
the routable prefix the border router favours for Thread devices |
at |
active dataset timestamp |
tv, vn, mn |
Thread version, vendor, model |
my ESP's firmware fills sb, omr and at differently from OpenThread's own
border router: no role bits, and its own prefix in omr. i read its role from the
REST API, not from mDNS.
decode the TXT records with python
no packages needed. it sends one mDNS query for each instance name and prints the fields above.
import socket, struct, sys, time
def qname(n):
return b"".join(bytes([len(p)]) + p.encode() for p in n.split(".")) + b"\0"
def read_name(buf, off):
end = None
while buf[off]:
if buf[off] & 0xC0 == 0xC0:
end = end or off + 2
off = ((buf[off] & 0x3F) << 8) | buf[off + 1]
else:
off += 1 + buf[off]
return end or off + 1
def txt(instance):
name = f"{instance}._meshcop._udp.local"
pkt = struct.pack(">6H", 0, 0, 1, 0, 0, 0) + qname(name) + struct.pack(">HH", 16, 0x8001)
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.settimeout(0.5)
deadline = time.time() + 3
while time.time() < deadline:
s.sendto(pkt, ("224.0.0.251", 5353))
try:
buf, _ = s.recvfrom(9000)
except socket.timeout:
continue
counts = struct.unpack(">6H", buf[:12])
off = 12
for _ in range(counts[2]):
off = read_name(buf, off) + 4
for _ in range(sum(counts[3:])):
off = read_name(buf, off)
rtype, _, _, rlen = struct.unpack(">HHIH", buf[off:off + 10])
rdata = buf[off + 10:off + 10 + rlen]
off += 10 + rlen
if rtype == 16 and b"xp=" in rdata:
kv, i = {}, 0
while i < len(rdata):
k, _, v = rdata[i + 1:i + 1 + rdata[i]].partition(b"=")
kv[k.decode()] = v
i += 1 + rdata[i]
return kv
for inst in sys.argv[1:]:
kv = txt(inst) or {}
print(inst)
for k in ("nn", "xp", "pt", "sb", "omr", "at", "tv", "vn", "mn"):
if k in kv:
v = kv[k]
if k in ("nn", "tv", "vn", "mn"):
v = v.decode(errors="replace")
elif k == "omr":
v = f"{v[1:].hex()} /{v[0]}"
elif k in ("pt", "sb"):
v = f"{int.from_bytes(v, 'big')} (0x{v.hex()})"
else:
v = v.hex()
print(f" {k} = {v}")
the instance names are the ones dns-sd -B lists.
what each border router publishes¶
this comes from the network data. on the ESP's USB console, run:
each entry includes the hex Rloc16 of the router that published it (a400 is
router 41, 3800 router 14). ::/0 is a default route, a route with n in
its flags is a NAT64 prefix, service 5d is an SRP server and service 01 is a
backbone router. ot bbr names the primary.
this is what mine have published since the failover:
| app | ESP | |
|---|---|---|
| default route off the mesh | yes | yes |
| SRP server, where Thread devices register | yes | yes |
| routable (OMR) prefix | uses the ESP's | publishes it |
| backbone router | standby | primary |
| NAT64 prefix | no | yes |
one border router at a time holds the prefix and the primary backbone router. before the failover the app had both, and they did not move back when it returned. a border router only publishes its own prefix when the network has none, or only a lower preference one. that puts every router on one prefix without any setting.
Thread devices reach IPv4 only addresses through the ESP either way.
watching a failover¶
the ESP's REST API is enough to watch what happens when the app goes away, with no USB.
- turn the automation off, then stop the app
-
watch the routers' addresses and the ESP's own
curl -s http://esp-ot-br-33f0.local/diagnostics | jq -c '.[] | {router: (.Rloc16 / 1024 | floor), addresses: .IP6AddressList}' curl -s http://esp-ot-br-33f0.local/ipaddr | jq -c '.result[] | {address, preferred}'- an address ending
0:ff:fe00:fc38is the primary backbone router's,fc00the leader's,fc10upwards are services - an address outside the mesh-local prefix and
fe80::is in the routable prefix "preferred": falseis a deprecated address. the web UI dashboard shows the same list- in mDNS, bit 8 of
sbis set on the primary backbone router, see mDNS
- an address ending
-
turn the automation back on. it starts the app again
this is what mine did:
| after stopping the app | |
|---|---|
| about 5 minutes | the app's router dropped out. the ESP became primary backbone router and published its own prefix |
| straight after | addresses in the old prefix deprecated |
| 5 minutes later | those addresses removed. OpenThread's default is 300 seconds |
| automation back on | the app rejoined as the same router, standby, on the ESP's prefix |