| # Copyright (c) 2021-2023 Project CHIP Authors |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import logging |
| import os |
| import shlex |
| from enum import Enum, auto |
| from typing import Optional |
| |
| from .builder import Builder, BuilderOutput |
| |
| |
| class Esp32Board(Enum): |
| DevKitC = auto() |
| M5Stack = auto() |
| C3DevKit = auto() |
| QEMU = auto() |
| |
| |
| class Esp32App(Enum): |
| ALL_CLUSTERS = auto() |
| ALL_CLUSTERS_MINIMAL = auto() |
| ENERGY_MANAGEMENT = auto() |
| LIGHT = auto() |
| LOCK = auto() |
| SHELL = auto() |
| BRIDGE = auto() |
| TEMPERATURE_MEASUREMENT = auto() |
| TESTS = auto() |
| OTA_REQUESTOR = auto() |
| OTA_PROVIDER = auto() |
| |
| @property |
| def ExamplePath(self): |
| if self == Esp32App.ALL_CLUSTERS: |
| return 'examples/all-clusters-app' |
| elif self == Esp32App.ALL_CLUSTERS_MINIMAL: |
| return 'examples/all-clusters-minimal-app' |
| elif self == Esp32App.ENERGY_MANAGEMENT: |
| return 'examples/energy-management-app' |
| elif self == Esp32App.LIGHT: |
| return 'examples/lighting-app' |
| elif self == Esp32App.LOCK: |
| return 'examples/lock-app' |
| elif self == Esp32App.SHELL: |
| return 'examples/shell' |
| elif self == Esp32App.BRIDGE: |
| return 'examples/bridge-app' |
| elif self == Esp32App.TEMPERATURE_MEASUREMENT: |
| return 'examples/temperature-measurement-app' |
| elif self == Esp32App.OTA_REQUESTOR: |
| return 'examples/ota-requestor-app' |
| elif self == Esp32App.OTA_PROVIDER: |
| return 'examples/ota-provider-app' |
| elif self == Esp32App.TESTS: |
| return 'src/test_driver' |
| else: |
| raise Exception('Unknown app type: %r' % self) |
| |
| @property |
| def AppNamePrefix(self): |
| if self == Esp32App.ALL_CLUSTERS: |
| return 'chip-all-clusters-app' |
| elif self == Esp32App.ALL_CLUSTERS_MINIMAL: |
| return 'chip-all-clusters-minimal-app' |
| elif self == Esp32App.ENERGY_MANAGEMENT: |
| return 'chip-energy-management-app' |
| elif self == Esp32App.LIGHT: |
| return 'chip-lighting-app' |
| elif self == Esp32App.LOCK: |
| return 'chip-lock-app' |
| elif self == Esp32App.SHELL: |
| return 'chip-shell' |
| elif self == Esp32App.BRIDGE: |
| return 'chip-bridge-app' |
| elif self == Esp32App.TEMPERATURE_MEASUREMENT: |
| return 'chip-temperature-measurement-app' |
| elif self == Esp32App.OTA_REQUESTOR: |
| return 'chip-ota-requestor-app' |
| elif self == Esp32App.OTA_PROVIDER: |
| return 'chip-ota-provider-app' |
| elif self == Esp32App.TESTS: |
| return None |
| else: |
| raise Exception('Unknown app type: %r' % self) |
| |
| @property |
| def FlashBundleName(self): |
| if not self.AppNamePrefix: |
| return None |
| |
| return self.AppNamePrefix + '.flashbundle.txt' |
| |
| def IsCompatible(self, board: Esp32Board): |
| if board == Esp32Board.QEMU: |
| return self == Esp32App.TESTS |
| elif board == Esp32Board.C3DevKit: |
| return self == Esp32App.ALL_CLUSTERS or self == Esp32App.ALL_CLUSTERS_MINIMAL |
| else: |
| return (board in {Esp32Board.M5Stack, Esp32Board.DevKitC}) and (self != Esp32App.TESTS) |
| |
| |
| def DefaultsFileName(board: Esp32Board, app: Esp32App, enable_rpcs: bool): |
| rpc_enabled_apps = {Esp32App.ALL_CLUSTERS, |
| Esp32App.ALL_CLUSTERS_MINIMAL, |
| Esp32App.LIGHT, |
| Esp32App.OTA_REQUESTOR, |
| Esp32App.OTA_PROVIDER, |
| Esp32App.TEMPERATURE_MEASUREMENT} |
| if app == Esp32App.TESTS: |
| return 'sdkconfig_qemu.defaults' |
| elif app not in rpc_enabled_apps: |
| return 'sdkconfig.defaults' |
| |
| rpc = "_rpc" if enable_rpcs else "" |
| if board == Esp32Board.DevKitC: |
| return 'sdkconfig{}.defaults'.format(rpc) |
| elif board == Esp32Board.M5Stack: |
| # a subset of apps have m5stack specific configurations. However others |
| # just compile for the same devices as aDevKitC |
| specific_apps = { |
| Esp32App.ALL_CLUSTERS, |
| Esp32App.ALL_CLUSTERS_MINIMAL, |
| Esp32App.LIGHT, |
| Esp32App.OTA_REQUESTOR, |
| } |
| if app in specific_apps: |
| return 'sdkconfig_m5stack{}.defaults'.format(rpc) |
| else: |
| return 'sdkconfig{}.defaults'.format(rpc) |
| elif board == Esp32Board.C3DevKit: |
| return 'sdkconfig{}.defaults.esp32c3'.format(rpc) |
| else: |
| raise Exception('Unknown board type') |
| |
| |
| class Esp32Builder(Builder): |
| |
| def __init__(self, |
| root, |
| runner, |
| board: Esp32Board = Esp32Board.M5Stack, |
| app: Esp32App = Esp32App.ALL_CLUSTERS, |
| enable_rpcs: bool = False, |
| enable_ipv4: bool = True, |
| enable_insights_trace: bool = False, |
| data_model_interface: Optional[str] = None, |
| ): |
| super(Esp32Builder, self).__init__(root, runner) |
| self.board = board |
| self.app = app |
| self.enable_rpcs = enable_rpcs |
| self.enable_ipv4 = enable_ipv4 |
| self.enable_insights_trace = enable_insights_trace |
| self.data_model_interface = data_model_interface |
| |
| if not app.IsCompatible(board): |
| raise Exception( |
| "Incompatible app/board combination: %r and %r", app, board) |
| |
| def _IdfEnvExecute(self, cmd, title=None): |
| # Run activate.sh after export.sh to ensure using the chip environment. |
| self._Execute( |
| ['bash', '-c', 'source $IDF_PATH/export.sh; source scripts/activate.sh; %s' % cmd], |
| title=title) |
| |
| @property |
| def ExamplePath(self): |
| return os.path.join(self.app.ExamplePath, 'esp32') |
| |
| def generate(self): |
| if os.path.exists(os.path.join(self.output_dir, 'build.ninja')): |
| return |
| |
| defaults = os.path.join(self.ExamplePath, DefaultsFileName( |
| self.board, self.app, self.enable_rpcs)) |
| |
| if not self._runner.dry_run and not os.path.exists(defaults): |
| raise Exception('SDK defaults file missing: %s' % defaults) |
| |
| defaults_out = os.path.join(self.output_dir, 'sdkconfig.defaults') |
| |
| self._Execute(['mkdir', '-p', self.output_dir], |
| title='Generating ' + self.identifier) |
| self._Execute(['cp', defaults, defaults_out]) |
| self._Execute( |
| ['rm', '-f', os.path.join(self.ExamplePath, 'sdkconfig')]) |
| |
| if not self.enable_ipv4: |
| self._Execute( |
| ['bash', '-c', 'echo -e "\\nCONFIG_DISABLE_IPV4=y\\n" >>%s' % shlex.quote(defaults_out)]) |
| self._Execute( |
| ['bash', '-c', 'echo -e "\\nCONFIG_LWIP_IPV4=n\\n" >>%s' % shlex.quote(defaults_out)]) |
| |
| if self.enable_insights_trace: |
| insights_flag = 'y' |
| else: |
| insights_flag = 'n' |
| |
| # pre-requisite |
| self._Execute( |
| ['bash', '-c', 'echo -e "\\nCONFIG_ESP_INSIGHTS_ENABLED=%s\\nCONFIG_ENABLE_ESP_INSIGHTS_TRACE=%s\\n" >>%s' % (insights_flag, insights_flag, shlex.quote(defaults_out))]) |
| |
| cmake_flags = [] |
| |
| if self.options.pregen_dir: |
| cmake_flags.append( |
| f"-DCHIP_CODEGEN_PREGEN_DIR={shlex.quote(self.options.pregen_dir)}") |
| |
| if self.data_model_interface: |
| cmake_flags.append(f'-DCHIP_DATA_MODEL_INTERFACE={self.data_model_interface}') |
| |
| cmake_args = ['-C', self.ExamplePath, '-B', |
| shlex.quote(self.output_dir)] + cmake_flags |
| |
| cmake_args = " ".join(cmake_args) |
| defaults = shlex.quote(defaults_out) |
| |
| cmd = f"\nexport SDKCONFIG_DEFAULTS={defaults}\nidf.py {cmake_args} reconfigure" |
| |
| # This will do a 'cmake reconfigure' which will create ninja files without rebuilding |
| self._IdfEnvExecute(cmd) |
| |
| def _build(self): |
| logging.info('Compiling Esp32 at %s', self.output_dir) |
| |
| # Unfortunately sdkconfig is sticky and needs reset on every build |
| self._Execute( |
| ['rm', '-f', os.path.join(self.ExamplePath, 'sdkconfig')]) |
| |
| defaults_out = os.path.join(self.output_dir, 'sdkconfig.defaults') |
| |
| # "ninja -C" is insufficient because sdkconfig changes on every 'config' and results |
| # in a full reconfiguration with default values |
| # |
| # This does a regen + reconfigure. |
| cmd = "\nexport SDKCONFIG_DEFAULTS={defaults}\nidf.py -C {example_path} -B {out} build".format( |
| defaults=shlex.quote(defaults_out), |
| example_path=self.ExamplePath, |
| out=shlex.quote(self.output_dir) |
| ) |
| |
| self._IdfEnvExecute(cmd, title='Building ' + self.identifier) |
| |
| def build_outputs(self): |
| if self.app == Esp32App.TESTS: |
| # Include the runnable image names as artifacts |
| with open(os.path.join(self.output_dir, 'test_images.txt'), 'rt') as f: |
| for name in filter(None, [x.strip() for x in f.readlines()]): |
| yield BuilderOutput(os.path.join(self.output_dir, name), name) |
| return |
| |
| extensions = ["elf"] |
| if self.options.enable_link_map_file: |
| extensions.append("map") |
| for ext in extensions: |
| name = f"{self.app.AppNamePrefix}.{ext}" |
| yield BuilderOutput(os.path.join(self.output_dir, name), name) |
| |
| def bundle_outputs(self): |
| if not self.app.FlashBundleName: |
| return |
| with open(os.path.join(self.output_dir, self.app.FlashBundleName)) as f: |
| for line in filter(None, [x.strip() for x in f.readlines()]): |
| yield BuilderOutput(os.path.join(self.output_dir, line), line) |