Skip to content

Commit 9bb0cf7

Browse files
authored
Proper child table restoration (#686)
* Restore child data more reliably to NVRAM * Remove NVRAM token writing * Revert `NV3ChildTableEntry` * Re-add NVRAM stuff * Ensure we reset after writing to NVRAM * Clean things up * Tests * Ensure things work without NV3 interface
1 parent 658adce commit 9bb0cf7

File tree

4 files changed

+301
-14
lines changed

4 files changed

+301
-14
lines changed

bellows/ezsp/v10/__init__.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import voluptuous
77

88
import bellows.config
9+
from bellows.exception import InvalidCommandError
910
import bellows.types as t
1011

1112
from . import commands, config
@@ -39,3 +40,47 @@ async def write_child_data(self, children: dict[t.EUI64, t.NWK]) -> None:
3940
timeout_remaining=0,
4041
),
4142
)
43+
44+
# There is unfortunately an SDK bug with `setChildData`: some sort of
45+
# internal flag in the NVRAM child table is not correctly set (0x00). For
46+
# working coordinators, it holds the value 0x80. We need to carefully tweak
47+
# this value to ensure restoration works 100%.
48+
try:
49+
rsp = await self.getTokenData(
50+
token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=index
51+
)
52+
if t.sl_Status.from_ember_status(rsp.status) != t.sl_Status.OK:
53+
LOGGER.warning(
54+
"Failed to read NVRAM child info for %d: %r", index, rsp
55+
)
56+
continue
57+
except (InvalidCommandError, AttributeError):
58+
LOGGER.debug("NV3 interface not available, skipping")
59+
continue
60+
61+
# We need to be careful and ensure that the value in NVRAM matches our
62+
# expected format (other than the flag byte)
63+
expected_entry = t.NV3ChildTableEntry(eui64=eui64, id=nwk, flags=0x80)
64+
65+
if rsp.value != expected_entry.replace(flags=rsp.value[-1]).serialize():
66+
LOGGER.warning(
67+
"Unexpected NVRAM child info for %d: %r, expected %r",
68+
index,
69+
rsp.value,
70+
expected_entry.serialize(),
71+
)
72+
continue
73+
74+
# Once we have it fully parsed, write in the correct value (if necessary).
75+
# The reason we do this roundabout read/write dance is because we can't be
76+
# sure the format in NVRAM will be static.
77+
entry, remaining = t.NV3ChildTableEntry.deserialize(rsp.value)
78+
assert not remaining
79+
80+
if entry.flags != 0x80:
81+
(status,) = await self.setTokenData(
82+
token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE,
83+
index=index,
84+
token_data=expected_entry.serialize(),
85+
)
86+
assert t.sl_Status.from_ember_status(status) == t.sl_Status.OK

bellows/types/struct.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -717,6 +717,14 @@ class NV3StackNetworkManagementToken(EzspStruct):
717717
padding: basic.uint8_t
718718

719719

720+
class NV3ChildTableEntry(EzspStruct):
721+
"""NV3 node table entry token value."""
722+
723+
eui64: named.EUI64
724+
id: named.NWK
725+
flags: basic.uint8_t
726+
727+
720728
class SlRxPacketInfo(EzspStruct):
721729
"""Received packet information.
722730

bellows/zigbee/application.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -413,14 +413,6 @@ async def write_network_info(
413413

414414
await ezsp.write_link_keys(network_info.key_table)
415415

416-
children_with_nwk_addresses = {
417-
eui64: network_info.nwk_addresses[eui64]
418-
for eui64 in network_info.children
419-
if eui64 in network_info.nwk_addresses
420-
}
421-
422-
await ezsp.write_child_data(children_with_nwk_addresses)
423-
424416
# Set the network settings
425417
parameters = t.EmberNetworkParameters()
426418
parameters.panId = t.EmberPanId(network_info.pan_id)
@@ -439,6 +431,17 @@ async def write_network_info(
439431
if network_info.nwk_update_id != 0:
440432
await ezsp.write_nwk_update_id(network_info.nwk_update_id)
441433

434+
children_with_nwk_addresses = {
435+
eui64: network_info.nwk_addresses[eui64]
436+
for eui64 in network_info.children
437+
if eui64 in network_info.nwk_addresses
438+
}
439+
440+
# Resetting the adapter after writing the child table is important! Otherwise,
441+
# NVRAM will not be fully re-read, causing issues.
442+
await ezsp.write_child_data(children_with_nwk_addresses)
443+
await self._reset()
444+
442445
await self._ensure_network_running()
443446

444447
async def reset_network_info(self):

tests/test_ezsp_v10.py

Lines changed: 237 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import pytest
44

5+
from bellows.exception import InvalidCommandError
6+
from bellows.ezsp.v9.commands import GetTokenDataRsp
57
import bellows.ezsp.v10
68
import bellows.types as t
79

@@ -48,12 +50,32 @@ async def test_pre_permit(ezsp_f):
4850
async def test_write_child_data(ezsp_f) -> None:
4951
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
5052

51-
await ezsp_f.write_child_data(
52-
{
53-
t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B,
54-
t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"): 0x1234,
55-
}
56-
)
53+
# Mock NVRAM entries with correct flags
54+
eui64_1 = t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57")
55+
eui64_2 = t.EUI64.convert("00:18:4b:00:1c:a1:b8:46")
56+
57+
def mock_get_token_data(token, index):
58+
if index == 0:
59+
return GetTokenDataRsp(
60+
status=t.EmberStatus.SUCCESS,
61+
value=t.NV3ChildTableEntry(
62+
eui64=eui64_1, id=0xC06B, flags=0x80
63+
).serialize(),
64+
)
65+
elif index == 1:
66+
return GetTokenDataRsp(
67+
status=t.EmberStatus.SUCCESS,
68+
value=t.NV3ChildTableEntry(
69+
eui64=eui64_2, id=0x1234, flags=0x80
70+
).serialize(),
71+
)
72+
else:
73+
return GetTokenDataRsp(status=t.EmberStatus.ERR_FATAL, value=b"")
74+
75+
ezsp_f.getTokenData = AsyncMock(side_effect=mock_get_token_data)
76+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
77+
78+
await ezsp_f.write_child_data({eui64_1: 0xC06B, eui64_2: 0x1234})
5779

5880
assert ezsp_f.setChildData.mock_calls == [
5981
call(
@@ -81,3 +103,212 @@ async def test_write_child_data(ezsp_f) -> None:
81103
),
82104
),
83105
]
106+
107+
assert ezsp_f.getTokenData.mock_calls == [
108+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0),
109+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=1),
110+
]
111+
assert ezsp_f.setTokenData.mock_calls == []
112+
113+
114+
async def test_write_child_data_nvram_read_failure(ezsp_f) -> None:
115+
"""Test write_child_data when NVRAM read fails for some entries."""
116+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
117+
118+
def mock_get_token_data(token, index):
119+
if index == 0:
120+
return GetTokenDataRsp(status=t.EmberStatus.ERR_FATAL, value=b"")
121+
elif index == 1:
122+
return GetTokenDataRsp(
123+
status=t.EmberStatus.SUCCESS,
124+
value=t.NV3ChildTableEntry(
125+
eui64=t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"),
126+
id=0x1234,
127+
flags=0x80,
128+
).serialize(),
129+
)
130+
else:
131+
return GetTokenDataRsp(status=t.EmberStatus.ERR_FATAL, value=b"")
132+
133+
ezsp_f.getTokenData = AsyncMock(side_effect=mock_get_token_data)
134+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
135+
136+
await ezsp_f.write_child_data(
137+
{
138+
t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B,
139+
t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"): 0x1234,
140+
}
141+
)
142+
143+
assert ezsp_f.getTokenData.mock_calls == [
144+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0),
145+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=1),
146+
]
147+
assert ezsp_f.setTokenData.mock_calls == []
148+
149+
150+
async def test_write_child_data_nvram_format_mismatch(ezsp_f) -> None:
151+
"""Test write_child_data when NVRAM format doesn't match expected format."""
152+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
153+
154+
ezsp_f.getTokenData = AsyncMock(
155+
return_value=GetTokenDataRsp(
156+
status=t.EmberStatus.SUCCESS,
157+
value=b"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b", # Wrong format
158+
)
159+
)
160+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
161+
162+
await ezsp_f.write_child_data({t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B})
163+
164+
assert ezsp_f.getTokenData.mock_calls == [
165+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0)
166+
]
167+
# No setTokenData due to format mismatch
168+
assert ezsp_f.setTokenData.mock_calls == []
169+
170+
171+
async def test_write_child_data_nvram_flags_correction_needed(ezsp_f) -> None:
172+
"""Test write_child_data when NVRAM flags need correction."""
173+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
174+
175+
ezsp_f.getTokenData = AsyncMock(
176+
return_value=GetTokenDataRsp(
177+
status=t.EmberStatus.SUCCESS,
178+
value=t.NV3ChildTableEntry(
179+
eui64=t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"), id=0xC06B, flags=0x00
180+
).serialize(),
181+
)
182+
)
183+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
184+
185+
await ezsp_f.write_child_data({t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B})
186+
187+
assert ezsp_f.getTokenData.mock_calls == [
188+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0)
189+
]
190+
191+
assert ezsp_f.setTokenData.mock_calls == [
192+
call(
193+
token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE,
194+
index=0,
195+
token_data=t.NV3ChildTableEntry(
196+
eui64=t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"), id=0xC06B, flags=0x80
197+
).serialize(),
198+
)
199+
]
200+
201+
202+
async def test_write_child_data_nvram_flags_already_correct(ezsp_f) -> None:
203+
"""Test write_child_data when NVRAM flags are already correct."""
204+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
205+
206+
ezsp_f.getTokenData = AsyncMock(
207+
return_value=GetTokenDataRsp(
208+
status=t.EmberStatus.SUCCESS,
209+
value=t.NV3ChildTableEntry(
210+
eui64=t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"), id=0xC06B, flags=0x80
211+
).serialize(),
212+
)
213+
)
214+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
215+
216+
await ezsp_f.write_child_data({t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B})
217+
218+
assert ezsp_f.getTokenData.mock_calls == [
219+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0)
220+
]
221+
assert ezsp_f.setTokenData.mock_calls == []
222+
223+
224+
async def test_write_child_data_nvram_set_token_failure(ezsp_f) -> None:
225+
"""Test write_child_data when setTokenData fails during flag correction."""
226+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
227+
228+
ezsp_f.getTokenData = AsyncMock(
229+
return_value=GetTokenDataRsp(
230+
status=t.EmberStatus.SUCCESS,
231+
value=t.NV3ChildTableEntry(
232+
eui64=t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"), id=0xC06B, flags=0x00
233+
).serialize(),
234+
)
235+
)
236+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.ERR_FATAL])
237+
238+
with pytest.raises(AssertionError):
239+
await ezsp_f.write_child_data(
240+
{t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B}
241+
)
242+
243+
244+
async def test_write_child_data_multiple_entries_mixed_scenarios(ezsp_f) -> None:
245+
"""Test write_child_data with multiple entries covering various scenarios."""
246+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
247+
248+
def mock_get_token_data(token, index):
249+
if index == 0: # Read failure
250+
return GetTokenDataRsp(status=t.EmberStatus.ERR_FATAL, value=b"")
251+
elif index == 1: # Needs correction
252+
return GetTokenDataRsp(
253+
status=t.EmberStatus.SUCCESS,
254+
value=t.NV3ChildTableEntry(
255+
eui64=t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"),
256+
id=0x1234,
257+
flags=0x00,
258+
).serialize(),
259+
)
260+
elif index == 2: # Already correct
261+
return GetTokenDataRsp(
262+
status=t.EmberStatus.SUCCESS,
263+
value=t.NV3ChildTableEntry(
264+
eui64=t.EUI64.convert("00:22:33:44:55:66:77:88"),
265+
id=0x5678,
266+
flags=0x80,
267+
).serialize(),
268+
)
269+
return GetTokenDataRsp(status=t.EmberStatus.ERR_FATAL, value=b"")
270+
271+
ezsp_f.getTokenData = AsyncMock(side_effect=mock_get_token_data)
272+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
273+
274+
await ezsp_f.write_child_data(
275+
{
276+
t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B,
277+
t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"): 0x1234,
278+
t.EUI64.convert("00:22:33:44:55:66:77:88"): 0x5678,
279+
}
280+
)
281+
282+
assert ezsp_f.getTokenData.mock_calls == [
283+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0),
284+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=1),
285+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=2),
286+
]
287+
288+
# Only entry 2 needs flag correction
289+
assert ezsp_f.setTokenData.mock_calls == [
290+
call(
291+
token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE,
292+
index=1,
293+
token_data=t.NV3ChildTableEntry(
294+
eui64=t.EUI64.convert("00:18:4b:00:1c:a1:b8:46"), id=0x1234, flags=0x80
295+
).serialize(),
296+
)
297+
]
298+
299+
300+
async def test_write_child_data_nv3_interface_unavailable(ezsp_f) -> None:
301+
"""Test write_child_data when NV3 interface is not available."""
302+
ezsp_f.setChildData.return_value = [t.EmberStatus.SUCCESS]
303+
ezsp_f.getTokenData = AsyncMock(
304+
side_effect=InvalidCommandError("NV3 not available")
305+
)
306+
ezsp_f.setTokenData = AsyncMock(return_value=[t.EmberStatus.SUCCESS])
307+
308+
# Should complete without raising an exception
309+
await ezsp_f.write_child_data({t.EUI64.convert("00:0b:57:ff:fe:2b:d4:57"): 0xC06B})
310+
311+
assert ezsp_f.getTokenData.mock_calls == [
312+
call(token=t.NV3KeyId.NVM3KEY_STACK_CHILD_TABLE, index=0)
313+
]
314+
assert ezsp_f.setTokenData.mock_calls == []

0 commit comments

Comments
 (0)