Skip to content

Commit 6c0bf5c

Browse files
Damian-Nordickkasperczyk-no
authored andcommitted
[nrf noup] Add custom west commands to ease ZAP usage
Add "zap-gui" west command that runs ZAP GUI to edit the requested ZAP file. Add "zap-generate" west command that generates the cluster code out of the requested ZAP file. Both commands verify if a correct version of ZAP package is installed in .zap-install directory in the Matter SDK module, and update the installed version accordingly. The commands also try to find the ZAP file in the current directory if the ZAP file path is not explicitly provided.
1 parent ae2dce5 commit 6c0bf5c

File tree

5 files changed

+332
-0
lines changed

5 files changed

+332
-0
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ examples/thermostat/ameba/build
7575
# Downloaded zap without a pigweed root (via zap_download.py)
7676
.zap
7777

78+
# Downloaded zap via west commands
79+
.zap-install
80+
7881
# When building esp-idf application, if any component is fetched using idf-component-manager then they are stored in
7982
# managed_component directory. Along with that dependencies.lock file is generated.
8083
# https://github.com/espressif/idf-component-manager#using-with-a-project

scripts/west/west-commands.yml

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Keep the help strings in sync with the values in the .py files!
2+
west-commands:
3+
- file: scripts/west/zap_generate.py
4+
commands:
5+
- name: zap-generate
6+
class: ZapGenerate
7+
help: Generate ZAP code
8+
- file: scripts/west/zap_gui.py
9+
commands:
10+
- name: zap-gui
11+
class: ZapGui
12+
help: Run Matter ZCL Advanced Platform (ZAP) GUI

scripts/west/zap_common.py

+186
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
# Copyright (c) 2024 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
import argparse
6+
import os
7+
import platform
8+
import re
9+
import shutil
10+
import stat
11+
import subprocess
12+
import tempfile
13+
import wget
14+
15+
from pathlib import Path
16+
from typing import Tuple
17+
from zipfile import ZipFile
18+
19+
from west import log
20+
21+
MATTER_PATH = Path(__file__).parents[2]
22+
23+
24+
def find_zap(root: Path = Path.cwd(), max_depth: int = 1):
25+
"""
26+
Find *.zap file in the given directory or its subdirectories.
27+
"""
28+
subdirs = []
29+
for name in root.iterdir():
30+
if name.is_file() and (name.suffix.lower() == '.zap'):
31+
return root / name
32+
if name.is_dir() and (max_depth > 0):
33+
subdirs.append(name)
34+
for subdir in subdirs:
35+
if zap := find_zap(root / subdir, max_depth - 1):
36+
return zap
37+
return None
38+
39+
40+
def existing_file_path(arg: str) -> Path:
41+
"""
42+
Helper function to validate file path argument.
43+
"""
44+
p = Path(arg)
45+
if p.is_file():
46+
return p
47+
raise argparse.ArgumentTypeError(f'invalid file path: \'{arg}\'')
48+
49+
50+
class ZapInstaller:
51+
INSTALL_DIR = Path('.zap-install')
52+
ZAP_URL_PATTERN = 'https://github.com/project-chip/zap/releases/download/v%04d.%02d.%02d-nightly/%s.zip'
53+
54+
def __init__(self, matter_path: Path):
55+
self.matter_path = matter_path
56+
self.install_path = matter_path / ZapInstaller.INSTALL_DIR
57+
58+
def unzip_darwin(zip: Path, out: Path):
59+
subprocess.check_call(['unzip', zip, '-d', out])
60+
61+
def unzip(zip: Path, out: Path):
62+
f = ZipFile(zip)
63+
f.extractall(out)
64+
f.close()
65+
66+
current_os = platform.system()
67+
if current_os == 'Linux':
68+
self.package = 'zap-linux-x64'
69+
self.zap_exe = 'zap'
70+
self.zap_cli_exe = 'zap-cli'
71+
self.unzip = unzip
72+
elif current_os == 'Windows':
73+
self.package = 'zap-win-x64'
74+
self.zap_exe = 'zap.exe'
75+
self.zap_cli_exe = 'zap-cli.exe'
76+
self.unzip = unzip
77+
elif current_os == 'Darwin':
78+
self.package = 'zap-mac-x64'
79+
self.zap_exe = 'zap.app/Contents/MacOS/zap'
80+
self.zap_cli_exe = 'zap-cli'
81+
self.unzip = unzip_darwin
82+
else:
83+
raise RuntimeError(f"Unsupported platform: {current_os}")
84+
85+
def get_install_path(self) -> Path:
86+
"""
87+
Returns ZAP package installation directory.
88+
"""
89+
return self.install_path
90+
91+
def get_zap_path(self) -> Path:
92+
"""
93+
Returns path to ZAP GUI.
94+
"""
95+
return self.install_path / self.zap_exe
96+
97+
def get_zap_cli_path(self) -> Path:
98+
"""
99+
Returns path to ZAP CLI.
100+
"""
101+
return self.install_path / self.zap_cli_exe
102+
103+
def get_recommended_version(self) -> Tuple[int, int, int]:
104+
"""
105+
Returns ZAP package recommended version as a tuple of integers.
106+
107+
Parses zap_execution.py script from Matter SDK to determine the minimum
108+
required ZAP package version.
109+
"""
110+
RE_MIN_ZAP_VERSION = r'MIN_ZAP_VERSION\s*=\s*\'(\d+)\.(\d+)\.(\d+)'
111+
zap_execution_path = self.matter_path / 'scripts/tools/zap/zap_execution.py'
112+
113+
with open(zap_execution_path, 'r') as f:
114+
if match := re.search(RE_MIN_ZAP_VERSION, f.read()):
115+
return tuple(int(group) for group in match.groups())
116+
raise RuntimeError(f'Failed to find MIN_ZAP_VERSION in {zap_execution_path}')
117+
118+
def get_current_version(self) -> Tuple[int, int, int]:
119+
"""
120+
Returns ZAP package current version as a tuple of integers.
121+
122+
Parses the output of `zap --version` to determine the current ZAP
123+
package version. If the ZAP package has not been installed yet,
124+
the method returns None.
125+
"""
126+
try:
127+
output = subprocess.check_output(
128+
[self.get_zap_path(), '--version']).decode('ascii').strip()
129+
except Exception:
130+
return None
131+
132+
RE_VERSION = r'Version:\s*(\d+)\.(\d+)\.(\d+)'
133+
if match := re.search(RE_VERSION, output):
134+
return tuple(int(group) for group in match.groups())
135+
136+
raise RuntimeError("Failed to find version in ZAP output")
137+
138+
def install_zap(self, version: Tuple[int, int, int]) -> None:
139+
"""
140+
Downloads and unpacks selected ZAP package version.
141+
"""
142+
with tempfile.TemporaryDirectory() as temp_dir:
143+
url = ZapInstaller.ZAP_URL_PATTERN % (*version, self.package)
144+
log.inf(f'Downloading {url}...')
145+
146+
try:
147+
wget.download(url, out=temp_dir)
148+
except Exception as e:
149+
raise RuntimeError(f'Failed to download ZAP package from {url}: {e}')
150+
151+
shutil.rmtree(self.install_path, ignore_errors=True)
152+
153+
log.inf('') # Fix console after displaying wget progress bar
154+
log.inf(f'Unzipping ZAP package to {self.install_path}...')
155+
156+
try:
157+
self.unzip(Path(temp_dir) / f'{self.package}.zip', self.install_path)
158+
except Exception as e:
159+
raise RuntimeError(f'Failed to unzip ZAP package: {e}')
160+
161+
ZapInstaller.set_exec_permission(self.get_zap_path())
162+
ZapInstaller.set_exec_permission(self.get_zap_cli_path())
163+
164+
def update_zap_if_needed(self) -> None:
165+
"""
166+
Installs ZAP package if not up to date.
167+
168+
Installs or overrides the previous ZAP package installation if the
169+
current version does not match the recommended version.
170+
"""
171+
recommended_version = self.get_recommended_version()
172+
current_version = self.get_current_version()
173+
174+
if current_version == recommended_version:
175+
log.inf('ZAP is up to date: {0}.{1}.{2}'.format(*recommended_version))
176+
return
177+
178+
if current_version:
179+
log.inf('Found ZAP version: {0}.{1}.{2}'.format(*current_version))
180+
181+
log.inf('Installing ZAP version: {0}.{1}.{2}'.format(*recommended_version))
182+
self.install_zap(recommended_version)
183+
184+
@staticmethod
185+
def set_exec_permission(path: Path) -> None:
186+
os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC)

scripts/west/zap_generate.py

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Copyright (c) 2024 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
import argparse
6+
import os
7+
import sys
8+
9+
from pathlib import Path
10+
from textwrap import dedent
11+
12+
from west import log
13+
from west.commands import CommandError, WestCommand
14+
15+
from zap_common import existing_file_path, find_zap, ZapInstaller, MATTER_PATH
16+
17+
18+
class ZapGenerate(WestCommand):
19+
20+
def __init__(self):
21+
super().__init__(
22+
'zap-generate', # gets stored as self.name
23+
'Generate Matter data model files with ZAP', # self.help
24+
# self.description:
25+
dedent('''
26+
Generate Matter data model files with the use of ZAP Tool
27+
based on the .zap template file defined for your application.'''))
28+
29+
def do_add_parser(self, parser_adder):
30+
parser = parser_adder.add_parser(self.name,
31+
help=self.help,
32+
formatter_class=argparse.RawDescriptionHelpFormatter,
33+
description=self.description)
34+
parser.add_argument('-z', '--zap-file', type=existing_file_path,
35+
help='Path to data model configuration file (*.zap)')
36+
parser.add_argument('-o', '--output', type=Path,
37+
help='Path where to store the generated files')
38+
return parser
39+
40+
def do_run(self, args, unknown_args):
41+
if args.zap_file:
42+
zap_file_path = args.zap_file.absolute()
43+
else:
44+
zap_file_path = find_zap()
45+
46+
if not zap_file_path:
47+
raise CommandError("No valid .zap file provided")
48+
49+
if args.output:
50+
output_path = args.output
51+
else:
52+
output_path = zap_file_path.parent / "zap-generated"
53+
54+
app_templates_path = MATTER_PATH / "src/app/zap-templates/app-templates.json"
55+
zap_generate_path = MATTER_PATH / "scripts/tools/zap/generate.py"
56+
57+
zap_installer = ZapInstaller(MATTER_PATH)
58+
zap_installer.update_zap_if_needed()
59+
60+
# make sure that the generate.py script uses the proper zap_cli binary (handled by west)
61+
os.environ["ZAP_INSTALL_PATH"] = str(zap_installer.get_zap_cli_path().parent.absolute())
62+
63+
cmd = [sys.executable, zap_generate_path]
64+
cmd += [zap_file_path]
65+
cmd += ["-t", app_templates_path]
66+
cmd += ["-o", output_path]
67+
68+
self.check_call([str(x) for x in cmd])
69+
70+
log.inf(f"Done. Files generated in {output_path}")

scripts/west/zap_gui.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright (c) 2024 Nordic Semiconductor ASA
2+
#
3+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
4+
5+
import argparse
6+
7+
from pathlib import Path
8+
from textwrap import dedent
9+
10+
from zap_common import existing_file_path, find_zap, ZapInstaller, MATTER_PATH
11+
from west.commands import WestCommand
12+
13+
14+
class ZapGui(WestCommand):
15+
16+
def __init__(self):
17+
super().__init__(
18+
'zap-gui',
19+
'Run Matter ZCL Advanced Platform (ZAP) GUI',
20+
dedent('''
21+
Run Matter ZCL Advanced Platform (ZAP) GUI.
22+
23+
The ZAP GUI in a node.js tool for configuring the data model
24+
of a Matter application, which defines clusters, commands,
25+
attributes and events enabled for the given application.'''))
26+
27+
def do_add_parser(self, parser_adder):
28+
parser = parser_adder.add_parser(self.name,
29+
help=self.help,
30+
formatter_class=argparse.RawDescriptionHelpFormatter,
31+
description=self.description)
32+
parser.add_argument('-z', '--zap-file', type=existing_file_path,
33+
help='Path to data model configuration file (*.zap)')
34+
parser.add_argument('-j', '--zcl-json', type=existing_file_path,
35+
help='Path to data model definition file (zcl.json)')
36+
return parser
37+
38+
def do_run(self, args, unknown_args):
39+
if args.zap_file:
40+
zap_file_path = args.zap_file
41+
else:
42+
zap_file_path = find_zap()
43+
44+
if args.zcl_json:
45+
zcl_json_path = args.zcl_json.absolute()
46+
else:
47+
zcl_json_path = MATTER_PATH / 'src/app/zap-templates/zcl/zcl.json'
48+
49+
app_templates_path = MATTER_PATH / 'src/app/zap-templates/app-templates.json'
50+
51+
zap_installer = ZapInstaller(Path(MATTER_PATH))
52+
zap_installer.update_zap_if_needed()
53+
zap_cache_path = zap_installer.get_install_path() / ".zap"
54+
55+
cmd = [zap_installer.get_zap_path()]
56+
cmd += [zap_file_path] if zap_file_path else []
57+
cmd += ["--zcl", zcl_json_path]
58+
cmd += ["--gen", app_templates_path]
59+
cmd += ["--stateDirectory", zap_cache_path]
60+
61+
self.check_call([str(x) for x in cmd])

0 commit comments

Comments
 (0)