Skip to content

Commit 286ca9d

Browse files
authored
Merge pull request #92 from puddly/puddly/nvram-migrations
Fix broken empty address manager entries after restore on affected devices
2 parents 37d6d44 + 8510035 commit 286ca9d

File tree

7 files changed

+212
-27
lines changed

7 files changed

+212
-27
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import pytest
2+
3+
import zigpy_znp.const as const
4+
import zigpy_znp.types as t
5+
import zigpy_znp.commands as c
6+
from zigpy_znp.types.nvids import ExNvIds, OsalNvIds
7+
8+
from ..conftest import FORMED_DEVICES, FormedZStack3CC2531
9+
10+
pytestmark = [pytest.mark.asyncio]
11+
12+
13+
@pytest.mark.parametrize("device", FORMED_DEVICES)
14+
async def test_addrmgr_empty_entries(make_connected_znp, device):
15+
znp, znp_server = await make_connected_znp(server_cls=device)
16+
17+
if znp.version >= 3.30:
18+
entries = [
19+
entry
20+
async for entry in znp.nvram.read_table(
21+
item_id=ExNvIds.ADDRMGR,
22+
item_type=t.AddrMgrEntry,
23+
)
24+
]
25+
else:
26+
entries = list(
27+
await znp.nvram.osal_read(
28+
OsalNvIds.ADDRMGR, item_type=t.AddressManagerTable
29+
)
30+
)
31+
32+
num_empty = 0
33+
34+
for entry in entries:
35+
if entry.extAddr != t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"):
36+
continue
37+
38+
num_empty += 1
39+
40+
if znp.version >= 3.30:
41+
assert entry == const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3
42+
else:
43+
assert entry == const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1
44+
45+
assert num_empty > 0
46+
47+
48+
@pytest.mark.parametrize("device", [FormedZStack3CC2531])
49+
async def test_addrmgr_rewrite_fix(device, make_application, mocker):
50+
# Keep track of reads
51+
addrmgr_reads = []
52+
53+
correct_entry = t.AddrMgrEntry(
54+
type=t.AddrMgrUserType.Default,
55+
nwkAddr=0xFFFF,
56+
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),
57+
)
58+
59+
bad_entry = t.AddrMgrEntry(
60+
type=t.AddrMgrUserType(0xFF),
61+
nwkAddr=0xFFFF,
62+
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),
63+
)
64+
65+
app, znp_server = make_application(server_cls=device)
66+
znp_server.callback_for_response(
67+
c.SYS.OSALNVReadExt.Req(Id=OsalNvIds.ADDRMGR, Offset=0), addrmgr_reads.append
68+
)
69+
70+
nvram = znp_server._nvram[ExNvIds.LEGACY]
71+
old_addrmgr, _ = t.AddressManagerTable.deserialize(nvram[OsalNvIds.ADDRMGR])
72+
73+
# Ensure the table looks the way we expect
74+
assert old_addrmgr.count(correct_entry) == 58
75+
assert old_addrmgr.count(bad_entry) == 0
76+
77+
assert nvram[OsalNvIds.ADDRMGR] == b"".join([e.serialize() for e in old_addrmgr])
78+
79+
# Purposefully corrupt the empty entries
80+
nvram[OsalNvIds.ADDRMGR] = b"".join(
81+
[(bad_entry if e == correct_entry else e).serialize() for e in old_addrmgr]
82+
)
83+
assert old_addrmgr != nvram[OsalNvIds.ADDRMGR]
84+
85+
assert len(addrmgr_reads) == 0
86+
await app.startup()
87+
await app.shutdown()
88+
assert len(addrmgr_reads) == 2
89+
90+
# Bad entries have been fixed
91+
new_addrmgr, _ = t.AddressManagerTable.deserialize(nvram[OsalNvIds.ADDRMGR])
92+
assert new_addrmgr == old_addrmgr
93+
94+
# Migration has been created
95+
assert t.uint8_t.deserialize(nvram[OsalNvIds.ZIGPY_ZNP_MIGRATION_ID])[0] >= 1
96+
97+
# Will not be read again
98+
assert len(addrmgr_reads) == 2
99+
await app.startup()
100+
await app.shutdown()
101+
assert len(addrmgr_reads) == 2
102+
103+
# Will be migrated again if the migration NVID is deleted
104+
del nvram[OsalNvIds.ZIGPY_ZNP_MIGRATION_ID]
105+
106+
old_addrmgr2 = nvram[OsalNvIds.ADDRMGR]
107+
108+
assert len(addrmgr_reads) == 2
109+
await app.startup()
110+
await app.shutdown()
111+
assert len(addrmgr_reads) == 3
112+
113+
# But nothing will change
114+
assert nvram[OsalNvIds.ADDRMGR] == old_addrmgr2

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -978,7 +978,7 @@ def _create_network_nvram(self):
978978
super()._create_network_nvram()
979979
self._nvram[ExNvIds.LEGACY][OsalNvIds.APS_LINK_KEY_TABLE] = b"\xFF" * 20
980980
self._nvram[ExNvIds.ADDRMGR] = {
981-
addr: self.nvram_serialize(const.EMPTY_ADDR_MGR_ENTRY)
981+
addr: self.nvram_serialize(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3)
982982
for addr in range(0x0000, 0x0100 + 1)
983983
}
984984

@@ -1094,7 +1094,7 @@ def _create_network_nvram(self):
10941094
super()._create_network_nvram()
10951095
self._nvram[ExNvIds.LEGACY][OsalNvIds.APS_LINK_KEY_TABLE] = b"\xFF" * 17
10961096
self._nvram[ExNvIds.LEGACY][OsalNvIds.ADDRMGR] = 124 * self.nvram_serialize(
1097-
const.EMPTY_ADDR_MGR_ENTRY
1097+
const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1
10981098
)
10991099

11001100
def create_nib(self, _=None):

zigpy_znp/api.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,9 +191,10 @@ async def write_network_info(
191191

192192
from zigpy_znp.znp import security
193193

194-
# Delete any existing HAS_CONFIGURED_ZSTACK* NV items. One (or both) may fail.
194+
# Delete any existing NV items that store formation state
195195
await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK1)
196196
await self.nvram.osal_delete(OsalNvIds.HAS_CONFIGURED_ZSTACK3)
197+
await self.nvram.osal_delete(OsalNvIds.ZIGPY_ZNP_MIGRATION_ID)
197198
await self.nvram.osal_delete(OsalNvIds.BDBNODEISONANETWORK)
198199

199200
# Instruct Z-Stack to reset everything on the next boot

zigpy_znp/const.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212
)
1313
ZSTACK_CONFIGURE_SUCCESS = t.uint8_t(0x55)
1414

15-
EMPTY_ADDR_MGR_ENTRY = t.AddrMgrEntry(
15+
EMPTY_ADDR_MGR_ENTRY_ZSTACK1 = t.AddrMgrEntry(
16+
type=t.AddrMgrUserType.Default,
17+
nwkAddr=0xFFFF,
18+
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),
19+
)
20+
21+
EMPTY_ADDR_MGR_ENTRY_ZSTACK3 = t.AddrMgrEntry(
1622
type=t.AddrMgrUserType(0xFF),
1723
nwkAddr=0xFFFF,
1824
extAddr=t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"),

zigpy_znp/types/nvids.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ class OsalNvIds(BaseNvIds):
4040
HAS_CONFIGURED_ZSTACK1 = 0x0F00
4141
HAS_CONFIGURED_ZSTACK3 = 0x0060
4242

43+
# Although the docs say "IDs reserved for applications range from 0x0401 to 0x0FFF",
44+
# no OSAL NVID beyond 0x03FF is writable with the MT interface when using Z-Stack 3.
45+
ZIGPY_ZNP_MIGRATION_ID = 0x005F
46+
4347
# OSAL NV item IDs
4448
EXTADDR = 0x0001
4549
BOOTCOUNTER = 0x0002

zigpy_znp/zigbee/application.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import zigpy_znp.config as conf
3131
import zigpy_znp.commands as c
3232
from zigpy_znp.api import ZNP
33+
from zigpy_znp.znp import security
3334
from zigpy_znp.utils import combine_concurrent_calls
3435
from zigpy_znp.exceptions import CommandNotRecognized, InvalidCommandResponse
3536
from zigpy_znp.types.nvids import OsalNvIds
@@ -186,6 +187,9 @@ async def _startup(self, auto_form=False, force_form=False, read_only=False):
186187
self._znp = znp
187188
self._znp.set_application(self)
188189

190+
if not read_only and not force_form:
191+
await self._migrate_nvram()
192+
189193
self._bind_callbacks()
190194

191195
# Next, read out the NVRAM item that Zigbee2MQTT writes when it has configured
@@ -928,6 +932,58 @@ async def _set_led_mode(self, *, led: t.uint8_t, mode: c.util.LEDMode) -> None:
928932
except (asyncio.TimeoutError, CommandNotRecognized):
929933
LOGGER.info("This build of Z-Stack does not appear to support LED control")
930934

935+
async def _migrate_nvram(self):
936+
"""
937+
Migrates NVRAM entries using the `ZIGPY_ZNP_MIGRATION_ID` NVRAM item.
938+
"""
939+
940+
try:
941+
migration_id = await self._znp.nvram.osal_read(
942+
OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, item_type=t.uint8_t
943+
)
944+
except KeyError:
945+
migration_id = 0
946+
947+
initial_migration_id = migration_id
948+
949+
# Migration 1: empty `ADDRMGR` entries are version-dependent and were improperly
950+
# written for CC253x devices.
951+
#
952+
# This migration is stateless and can safely be run more than once:
953+
# the only downside is that startup times increase by 10s on newer
954+
# coordinators, which is why the migration ID is persisted.
955+
if migration_id < 1:
956+
try:
957+
entries = await security.read_addr_manager_entries(self._znp)
958+
except KeyError:
959+
pass
960+
else:
961+
fixed_entries = []
962+
963+
for entry in entries:
964+
if entry.extAddr != t.EUI64.convert("FF:FF:FF:FF:FF:FF:FF:FF"):
965+
fixed_entries.append(entry)
966+
elif self._znp.version == 3.30:
967+
fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3)
968+
else:
969+
fixed_entries.append(const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1)
970+
971+
if entries != fixed_entries:
972+
LOGGER.warning(
973+
"Repairing %d invalid empty address manager entries (total %d)",
974+
sum(i != j for i, j in zip(entries, fixed_entries)),
975+
len(entries),
976+
)
977+
await security.write_addr_manager_entries(self._znp, fixed_entries)
978+
979+
migration_id = 1
980+
981+
if initial_migration_id != migration_id:
982+
await self._znp.nvram.osal_write(
983+
OsalNvIds.ZIGPY_ZNP_MIGRATION_ID, t.uint8_t(migration_id), create=True
984+
)
985+
await self._znp.reset()
986+
931987
async def _write_stack_settings(self, *, reset_if_changed: bool) -> None:
932988
"""
933989
Writes network-independent Z-Stack settings to NVRAM.

zigpy_znp/znp/security.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ async def write_tc_frame_counter(
153153
)
154154

155155

156-
async def read_addr_mgr_entries(znp: ZNP) -> typing.Sequence[t.AddrMgrEntry]:
156+
async def read_addr_manager_entries(znp: ZNP) -> typing.Sequence[t.AddrMgrEntry]:
157157
if znp.version >= 3.30:
158158
entries = [
159159
entry
@@ -259,7 +259,7 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
259259
if znp.version > 1.2:
260260
tclk_seed = await znp.nvram.osal_read(OsalNvIds.TCLK_SEED, item_type=t.KeyData)
261261

262-
addr_mgr = await read_addr_mgr_entries(znp)
262+
addr_mgr = await read_addr_manager_entries(znp)
263263
devices = {}
264264

265265
for entry in addr_mgr:
@@ -317,30 +317,13 @@ async def read_devices(znp: ZNP) -> typing.Sequence[StoredDevice]:
317317

318318

319319
async def write_addr_manager_entries(
320-
znp: ZNP, devices: typing.Sequence[StoredDevice]
320+
znp: ZNP, entries: typing.Sequence[t.AddrMgrEntry]
321321
) -> None:
322-
entries = []
323-
324-
for dev in devices:
325-
entry = t.AddrMgrEntry(
326-
type=t.AddrMgrUserType.Default,
327-
nwkAddr=dev.node_info.nwk,
328-
extAddr=dev.node_info.ieee,
329-
)
330-
331-
if dev.key is not None:
332-
entry.type |= t.AddrMgrUserType.Security
333-
334-
if dev.is_child:
335-
entry.type |= t.AddrMgrUserType.Assoc
336-
337-
entries.append(entry)
338-
339322
if znp.version >= 3.30:
340323
await znp.nvram.write_table(
341324
item_id=ExNvIds.ADDRMGR,
342325
values=entries,
343-
fill_value=const.EMPTY_ADDR_MGR_ENTRY,
326+
fill_value=const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3,
344327
)
345328
return
346329

@@ -349,7 +332,11 @@ async def write_addr_manager_entries(
349332
old_entries = await znp.nvram.osal_read(
350333
OsalNvIds.ADDRMGR, item_type=t.AddressManagerTable
351334
)
352-
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY]
335+
336+
if znp.version >= 3.30:
337+
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY_ZSTACK3]
338+
else:
339+
new_entries = len(old_entries) * [const.EMPTY_ADDR_MGR_ENTRY_ZSTACK1]
353340

354341
# Purposefully throw an `IndexError` if we are trying to write too many entries
355342
for index, entry in enumerate(entries):
@@ -449,8 +436,25 @@ async def write_devices(
449436
if len(new_link_key_table_value) > len(old_link_key_table):
450437
raise RuntimeError("New link key table is larger than the current one")
451438

439+
addr_mgr_entries = []
440+
441+
for dev in devices:
442+
entry = t.AddrMgrEntry(
443+
type=t.AddrMgrUserType.Default,
444+
nwkAddr=dev.node_info.nwk,
445+
extAddr=dev.node_info.ieee,
446+
)
447+
448+
if dev.key is not None:
449+
entry.type |= t.AddrMgrUserType.Security
450+
451+
if dev.is_child:
452+
entry.type |= t.AddrMgrUserType.Assoc
453+
454+
addr_mgr_entries.append(entry)
455+
452456
# Postpone writes until all of the table entries have been created
453-
await write_addr_manager_entries(znp, devices)
457+
await write_addr_manager_entries(znp, addr_mgr_entries)
454458

455459
if old_link_key_table is None:
456460
return

0 commit comments

Comments
 (0)