Skip to content

Commit 42b14f9

Browse files
committed
Implement backup and restore
1 parent a148f10 commit 42b14f9

File tree

4 files changed

+174
-3
lines changed

4 files changed

+174
-3
lines changed

README.md

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,21 @@ custom_zha_radios:
2525
description: TI CC13x2, CC26x2, and ZZH
2626
```
2727
28+
# Backup and restore
29+
A complete radio NVRAM backup can be performed to migrate between different radios **based on the same chip** (i.e. between the zig-a-zig-ah! and the LAUNCHXL-CC26X2R1 will work). Anything else is untested.
30+
31+
```shell
32+
(venv) $ python -m zigpy_znp.tools.backup /dev/serial/by-id/old_radio -o backup.json
33+
(venv) $ python -m zigpy_znp.tools.restore /dev/serial/by-id/new_radio -i backup.json
34+
```
35+
2836
# Hardware requirements
2937
USB-adapters, GPIO-modules, and development-boards running recent TI Z-Stack releases (i.e. CC13x2 and CC26x2) are supported. Reference hardware for this project includes:
3038

3139
- [TI LAUNCHXL-CC26X2R1](https://www.ti.com/tool/LAUNCHXL-CC26X2R1) running [Z-Stack 3.30.00.03](https://github.com/Koenkk/Z-Stack-firmware/tree/master/coordinator/Z-Stack_3.x.0/bin). You can flash `CC26X2R1_20191106.hex` using [TI's UNIFLASH](https://www.ti.com/tool/download/UNIFLASH).
3240
- [Electrolama's zig-a-zig-ah!](https://electrolama.com/projects/zig-a-zig-ah/) running [Z-Stack 4.10.00.78](https://github.com/Koenkk/Z-Stack-firmware/tree/develop/coordinator/Z-Stack_3.x.0/bin). You can flash `CC26X2R1_20200417.hex` using [cc2538-bsl](https://github.com/JelmerT/cc2538-bsl).
3341

34-
Z-Stack versions 3.x and above are currently required and all communication with the radio module is done over the the Z-Stack Monitor and Test (MT) interface via a serial port.
35-
36-
Texas Instruments CC13x2 and CC26x2 based adapters/boards already come with a bootloader so can be flashed over USB using the official "Flash Programmer v2" from Texas Instruments. The zig-a-zig-ah!
42+
Z-Stack versions 3.x and above are currently required and all communication with the radio module is done over the the Z-Stack Monitor and Test (MT) interface via a serial port.
3743

3844
## Texas Instruments Chip Part Numbers
3945
Texas Instruments (TI) has quite a few different wireless MCU chips and they are all used/mentioned in open-source Zigbee world which can be daunting if you are just starting out. Here is a quick summary of part numbers and key features.

zigpy_znp/tools/__init__.py

Whitespace-only changes.

zigpy_znp/tools/backup.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import sys
2+
import json
3+
import asyncio
4+
import logging
5+
import argparse
6+
import coloredlogs
7+
8+
import zigpy_znp.types as t
9+
import zigpy_znp.commands as c
10+
11+
from zigpy_znp.api import ZNP
12+
from zigpy_znp.config import CONFIG_SCHEMA
13+
from zigpy_znp.exceptions import InvalidCommandResponse
14+
from zigpy_znp.types.nvids import NwkNvIds, OsalExNvIds
15+
16+
coloredlogs.install(level=logging.DEBUG)
17+
logging.getLogger("zigpy_znp").setLevel(logging.DEBUG)
18+
19+
LOGGER = logging.getLogger(__name__)
20+
21+
22+
async def backup(radio_path):
23+
znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}}))
24+
25+
await znp.connect()
26+
27+
data = {
28+
"osal": {},
29+
"nwk": {},
30+
}
31+
32+
for nwk_nvid in NwkNvIds:
33+
try:
34+
value = await znp.nvram_read(nwk_nvid)
35+
LOGGER.info("%s = %s", nwk_nvid, value)
36+
37+
data["nwk"][nwk_nvid.name] = value.hex()
38+
except InvalidCommandResponse:
39+
LOGGER.warning("Read failed for %s", nwk_nvid)
40+
continue
41+
42+
for osal_nvid in OsalExNvIds:
43+
length_rsp = await znp.request(
44+
c.SYS.NVLength.Req(SysId=1, ItemId=osal_nvid, SubId=0)
45+
)
46+
length = length_rsp.Length
47+
48+
if length == 0:
49+
LOGGER.warning("Read failed for %s", osal_nvid)
50+
continue
51+
52+
value = (
53+
await znp.request(
54+
c.SYS.NVRead.Req(
55+
SysId=1, ItemId=osal_nvid, SubId=0, Offset=0, Length=length
56+
),
57+
RspStatus=t.Status.SUCCESS,
58+
)
59+
).Value
60+
LOGGER.info("%s = %s", osal_nvid, value)
61+
62+
data["osal"][osal_nvid.name] = value.hex()
63+
64+
return data
65+
66+
67+
async def main(argv):
68+
parser = argparse.ArgumentParser(description="Backup a radio's NVRAM")
69+
parser.add_argument("serial", type=argparse.FileType("rb"), help="Serial port path")
70+
parser.add_argument(
71+
"--output", "-o", type=argparse.FileType("w"), help="Output file", default="-"
72+
)
73+
74+
args = parser.parse_args(argv)
75+
76+
# We just want to make sure it exists
77+
args.serial.close()
78+
79+
obj = await backup(args.serial.name)
80+
81+
args.output.write(json.dumps(obj, indent=4))
82+
83+
84+
if __name__ == "__main__":
85+
asyncio.run(main(sys.argv[1:]))

zigpy_znp/tools/restore.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import sys
2+
import json
3+
import asyncio
4+
import logging
5+
import argparse
6+
import coloredlogs
7+
8+
import zigpy_znp.types as t
9+
import zigpy_znp.commands as c
10+
11+
from zigpy_znp.api import ZNP
12+
from zigpy_znp.config import CONFIG_SCHEMA
13+
from zigpy_znp.exceptions import InvalidCommandResponse
14+
from zigpy_znp.types.nvids import NwkNvIds, OsalExNvIds
15+
16+
coloredlogs.install(level=logging.DEBUG)
17+
logging.getLogger("zigpy_znp").setLevel(logging.DEBUG)
18+
19+
LOGGER = logging.getLogger(__name__)
20+
21+
22+
async def restore(radio_path, backup):
23+
znp = ZNP(CONFIG_SCHEMA({"device": {"path": radio_path}}))
24+
25+
await znp.connect()
26+
27+
for nwk_nvid, value in backup["nwk"].items():
28+
nvid = NwkNvIds[nwk_nvid]
29+
value = bytes.fromhex(value)
30+
31+
# XXX: are any NVIDs not filled all the way?
32+
init_rsp = await znp.request(
33+
c.SYS.OSALNVItemInit.Req(Id=nvid, ItemLen=len(value), Value=value)
34+
)
35+
assert init_rsp.Status in (t.Status.SUCCESS, t.Status.NV_ITEM_UNINIT)
36+
37+
try:
38+
await znp.nvram_write(nvid, value)
39+
except InvalidCommandResponse:
40+
LOGGER.warning("Write failed for %s = %s", nvid, value)
41+
42+
for osal_nvid, value in backup["osal"].items():
43+
nvid = OsalExNvIds[osal_nvid]
44+
value = bytes.fromhex(value)
45+
46+
try:
47+
await znp.request(
48+
c.SYS.NVWrite.Req(SysId=1, ItemId=nvid, SubId=0, Offset=0, Value=value),
49+
RspStatus=t.Status.SUCCESS,
50+
)
51+
except InvalidCommandResponse:
52+
LOGGER.warning("Write failed for %s = %s", nvid, value)
53+
54+
# Reset afterwards to have the new values take effect
55+
await znp.request_callback_rsp(
56+
request=c.SYS.ResetReq.Req(Type=t.ResetType.Soft),
57+
callback=c.SYS.ResetInd.Callback(partial=True),
58+
)
59+
60+
61+
async def main(argv):
62+
parser = argparse.ArgumentParser(
63+
description="Restore a radio's NVRAM from a previous backup"
64+
)
65+
parser.add_argument("serial", type=argparse.FileType("rb"), help="Serial port path")
66+
parser.add_argument(
67+
"--input", "-i", type=argparse.FileType("r"), help="Input file", required=True
68+
)
69+
70+
args = parser.parse_args(argv)
71+
72+
# We just want to make sure it exists
73+
args.serial.close()
74+
75+
backup = json.load(args.input)
76+
await restore(args.serial.name, backup)
77+
78+
79+
if __name__ == "__main__":
80+
asyncio.run(main(sys.argv[1:]))

0 commit comments

Comments
 (0)