mirror of
https://github.com/esphome/esphome.git
synced 2024-12-24 14:34:54 +01:00
nrf52 core based on zephyr
This commit is contained in:
parent
4c6a17e304
commit
2ad8d7d1c5
28 changed files with 1215 additions and 9 deletions
|
@ -6,6 +6,7 @@ import os
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
|
||||
import argcomplete
|
||||
|
@ -36,6 +37,7 @@ from esphome.const import (
|
|||
PLATFORM_RP2040,
|
||||
PLATFORM_RTL87XX,
|
||||
SECRETS_FILES,
|
||||
PLATFORM_NRF52,
|
||||
)
|
||||
from esphome.core import CORE, EsphomeError, coroutine
|
||||
from esphome.helpers import indent, is_ip_address
|
||||
|
@ -47,6 +49,13 @@ from esphome.util import (
|
|||
get_serial_ports,
|
||||
)
|
||||
from esphome.log import color, setup_log, Fore
|
||||
from .zephyr_tools import (
|
||||
logger_scan,
|
||||
logger_connect,
|
||||
smpmgr_scan,
|
||||
smpmgr_upload,
|
||||
is_mac_address,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -86,19 +95,59 @@ def choose_prompt(options, purpose: str = None):
|
|||
def choose_upload_log_host(
|
||||
default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
|
||||
):
|
||||
try:
|
||||
mcuboot = CORE.config["nrf52"]["bootloader"] == "mcuboot"
|
||||
except KeyError:
|
||||
mcuboot = False
|
||||
try:
|
||||
ble_logger = CORE.config["zephyr_ble_nus"]["log"]
|
||||
except KeyError:
|
||||
ble_logger = False
|
||||
ota = "ota" in CORE.config
|
||||
options = []
|
||||
prefix = ""
|
||||
if mcuboot and show_ota and ota:
|
||||
prefix = "mcumgr "
|
||||
for port in get_serial_ports():
|
||||
options.append((f"{port.path} ({port.description})", port.path))
|
||||
options.append(
|
||||
(f"{prefix}{port.path} ({port.description})", f"{prefix}{port.path}")
|
||||
)
|
||||
if default == "SERIAL":
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if default == "OTA":
|
||||
return CORE.address
|
||||
if default == "PYOCD":
|
||||
if not mcuboot:
|
||||
raise EsphomeError("PYOCD for adafruit is not implemented")
|
||||
options = [("pyocd", "PYOCD")]
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
if not mcuboot:
|
||||
if (show_ota and ota) or (show_api and "api" in CORE.config):
|
||||
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||
if default == "OTA":
|
||||
return CORE.address
|
||||
elif show_ota and ota:
|
||||
if default:
|
||||
options.append((f"OTA over Bluetooth LE ({default})", f"mcumgr {default}"))
|
||||
return choose_prompt(options, purpose=purpose)
|
||||
ble_devices = asyncio.run(smpmgr_scan(CORE.config["esphome"]["name"]))
|
||||
if len(ble_devices) == 0:
|
||||
_LOGGER.warning("No OTA over Bluetooth LE service found!")
|
||||
for device in ble_devices:
|
||||
options.append(
|
||||
(
|
||||
f"OTA over Bluetooth LE({device.address}) {device.name}",
|
||||
f"mcumgr {device.address}",
|
||||
)
|
||||
)
|
||||
if show_mqtt and CONF_MQTT in CORE.config:
|
||||
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
|
||||
if default == "OTA":
|
||||
return "MQTT"
|
||||
if "logging" == purpose and ble_logger and default is None:
|
||||
ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"]))
|
||||
if ble_device:
|
||||
options.append((f"Bluetooth LE logger ({ble_device})", ble_device.address))
|
||||
else:
|
||||
_LOGGER.warning("No logger over Bluetooth LE service found!")
|
||||
if default is not None:
|
||||
return default
|
||||
if check_default is not None and check_default in [opt[1] for opt in options]:
|
||||
|
@ -111,6 +160,8 @@ def get_port_type(port):
|
|||
return "SERIAL"
|
||||
if port == "MQTT":
|
||||
return "MQTT"
|
||||
if is_mac_address(port):
|
||||
return "BLE"
|
||||
return "NETWORK"
|
||||
|
||||
|
||||
|
@ -289,10 +340,11 @@ def upload_using_esptool(config, port, file):
|
|||
return run_esptool(115200)
|
||||
|
||||
|
||||
def upload_using_platformio(config, port):
|
||||
def upload_using_platformio(config, port, upload_args=None):
|
||||
from esphome import platformio_api
|
||||
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
if upload_args is None:
|
||||
upload_args = ["-t", "upload", "-t", "nobuild"]
|
||||
if port is not None:
|
||||
upload_args += ["--upload-port", port]
|
||||
return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
|
||||
|
@ -329,7 +381,19 @@ def upload_program(config, args, host):
|
|||
if CORE.target_platform in (PLATFORM_BK72XX, PLATFORM_RTL87XX):
|
||||
return upload_using_platformio(config, host)
|
||||
|
||||
return 1 # Unknown target platform
|
||||
if CORE.target_platform in (PLATFORM_NRF52):
|
||||
return upload_using_platformio(config, host, ["-t", "upload"])
|
||||
|
||||
raise EsphomeError(f"Unknown target platform: {CORE.target_platform}")
|
||||
|
||||
if host == "PYOCD":
|
||||
print(CORE)
|
||||
return upload_using_platformio(config, host, ["-t", "flash_pyocd"])
|
||||
if host.startswith("mcumgr"):
|
||||
firmware = os.path.abspath(
|
||||
CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin")
|
||||
)
|
||||
return asyncio.run(smpmgr_upload(config, host.split(" ")[1], firmware))
|
||||
|
||||
ota_conf = {}
|
||||
for ota_item in config.get(CONF_OTA, []):
|
||||
|
@ -389,6 +453,9 @@ def show_logs(config, args, port):
|
|||
config, args.topic, args.username, args.password, args.client_id
|
||||
)
|
||||
|
||||
if get_port_type(port) == "BLE":
|
||||
return asyncio.run(logger_connect(port))
|
||||
|
||||
raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
|
||||
|
||||
|
||||
|
|
107
esphome/components/nrf52/__init__.py
Normal file
107
esphome/components/nrf52/__init__.py
Normal file
|
@ -0,0 +1,107 @@
|
|||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_BOARD,
|
||||
KEY_CORE,
|
||||
KEY_TARGET_FRAMEWORK,
|
||||
KEY_TARGET_PLATFORM,
|
||||
PLATFORM_NRF52,
|
||||
CONF_TYPE,
|
||||
CONF_FRAMEWORK,
|
||||
CONF_PLATFORM_VERSION,
|
||||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
|
||||
from esphome.components.zephyr import (
|
||||
zephyr_set_core_data,
|
||||
zephyr_to_code,
|
||||
)
|
||||
from esphome.components.zephyr.const import (
|
||||
KEY_ZEPHYR,
|
||||
KEY_BOOTLOADER,
|
||||
BOOTLOADER_MCUBOOT,
|
||||
)
|
||||
from .boards_zephyr import BOARDS_ZEPHYR
|
||||
from .const import (
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
)
|
||||
|
||||
# force import gpio to register pin schema
|
||||
from .gpio import nrf52_pin_to_code # noqa
|
||||
|
||||
AUTO_LOAD = ["zephyr"]
|
||||
|
||||
|
||||
def set_core_data(config):
|
||||
zephyr_set_core_data(config)
|
||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = KEY_ZEPHYR
|
||||
return config
|
||||
|
||||
|
||||
BOOTLOADERS = [
|
||||
BOOTLOADER_ADAFRUIT,
|
||||
BOOTLOADER_MCUBOOT,
|
||||
]
|
||||
|
||||
|
||||
def _detect_bootloader(value):
|
||||
value = value.copy()
|
||||
bootloader = None
|
||||
|
||||
if (
|
||||
value[CONF_BOARD] in BOARDS_ZEPHYR
|
||||
and KEY_BOOTLOADER in BOARDS_ZEPHYR[value[CONF_BOARD]]
|
||||
):
|
||||
bootloader = BOARDS_ZEPHYR[value[CONF_BOARD]][KEY_BOOTLOADER]
|
||||
|
||||
if KEY_BOOTLOADER not in value:
|
||||
if bootloader is None:
|
||||
bootloader = BOOTLOADER_MCUBOOT
|
||||
value[KEY_BOOTLOADER] = bootloader
|
||||
else:
|
||||
if bootloader is not None and bootloader != value[KEY_BOOTLOADER]:
|
||||
raise cv.Invalid(
|
||||
f"{value[CONF_FRAMEWORK][CONF_TYPE]} does not support '{bootloader}' bootloader for {value[CONF_BOARD]}"
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
CONFIG_SCHEMA = cv.All(
|
||||
cv.Schema(
|
||||
{
|
||||
cv.Required(CONF_BOARD): cv.string_strict,
|
||||
cv.Optional(KEY_BOOTLOADER): cv.one_of(*BOOTLOADERS, lower=True),
|
||||
}
|
||||
),
|
||||
_detect_bootloader,
|
||||
set_core_data,
|
||||
)
|
||||
|
||||
|
||||
@coroutine_with_priority(1000)
|
||||
async def to_code(config):
|
||||
cg.add_platformio_option("board", config[CONF_BOARD])
|
||||
cg.add_build_flag("-DUSE_NRF52")
|
||||
cg.add_define("ESPHOME_BOARD", config[CONF_BOARD])
|
||||
cg.add_define("ESPHOME_VARIANT", "NRF52")
|
||||
conf = {CONF_PLATFORM_VERSION: "platformio/nordicnrf52@10.3.0"}
|
||||
cg.add_platformio_option(CONF_FRAMEWORK, CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK])
|
||||
cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
|
||||
cg.add_platformio_option(
|
||||
"platform_packages",
|
||||
[
|
||||
"platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf",
|
||||
"platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng",
|
||||
],
|
||||
)
|
||||
|
||||
if config[KEY_BOOTLOADER] == BOOTLOADER_ADAFRUIT:
|
||||
# make sure that firmware.zip is created
|
||||
# for Adafruit_nRF52_Bootloader
|
||||
cg.add_platformio_option("board_upload.protocol", "nrfutil")
|
||||
cg.add_platformio_option("board_upload.use_1200bps_touch", "true")
|
||||
cg.add_platformio_option("board_upload.require_upload_port", "true")
|
||||
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
|
||||
#
|
||||
zephyr_to_code(conf)
|
6
esphome/components/nrf52/boards_zephyr.py
Normal file
6
esphome/components/nrf52/boards_zephyr.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from esphome.components.zephyr.const import KEY_BOOTLOADER
|
||||
from .const import BOOTLOADER_ADAFRUIT
|
||||
|
||||
BOARDS_ZEPHYR = {
|
||||
"adafruit_itsybitsy_nrf52840": {KEY_BOOTLOADER: BOOTLOADER_ADAFRUIT},
|
||||
}
|
1
esphome/components/nrf52/const.py
Normal file
1
esphome/components/nrf52/const.py
Normal file
|
@ -0,0 +1 @@
|
|||
BOOTLOADER_ADAFRUIT = "adafruit"
|
78
esphome/components/nrf52/gpio.py
Normal file
78
esphome/components/nrf52/gpio.py
Normal file
|
@ -0,0 +1,78 @@
|
|||
from esphome import pins
|
||||
|
||||
import esphome.codegen as cg
|
||||
import esphome.config_validation as cv
|
||||
from esphome.const import (
|
||||
CONF_ID,
|
||||
CONF_MODE,
|
||||
CONF_INVERTED,
|
||||
CONF_NUMBER,
|
||||
CONF_ANALOG,
|
||||
)
|
||||
from esphome.components.zephyr.const import (
|
||||
zephyr_ns,
|
||||
)
|
||||
|
||||
GPIOPin = zephyr_ns.class_("ZephyrGPIOPin", cg.InternalGPIOPin)
|
||||
|
||||
|
||||
def _translate_pin(value):
|
||||
if isinstance(value, dict) or value is None:
|
||||
raise cv.Invalid(
|
||||
"This variable only supports pin numbers, not full pin schemas "
|
||||
"(with inverted and mode)."
|
||||
)
|
||||
if isinstance(value, int):
|
||||
return value
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
pass
|
||||
# e.g. P0.27
|
||||
if len(value) >= len("P0.0") and value[0] == "P" and value[2] == ".":
|
||||
return cv.int_(value[len("P")].strip()) * 32 + cv.int_(
|
||||
value[len("P0.") :].strip()
|
||||
)
|
||||
raise cv.Invalid(f"Invalid pin: {value}")
|
||||
|
||||
|
||||
ADC_INPUTS = [
|
||||
"AIN0",
|
||||
"AIN1",
|
||||
"AIN2",
|
||||
"AIN3",
|
||||
"AIN4",
|
||||
"AIN5",
|
||||
"AIN6",
|
||||
"AIN7",
|
||||
"VDD",
|
||||
"VDDHDIV5",
|
||||
]
|
||||
|
||||
|
||||
def validate_gpio_pin(value):
|
||||
if value in ADC_INPUTS:
|
||||
return value
|
||||
value = _translate_pin(value)
|
||||
if value < 0 or value > (32 + 16):
|
||||
raise cv.Invalid(f"NRF52: Invalid pin number: {value}")
|
||||
return value
|
||||
|
||||
|
||||
NRF52_PIN_SCHEMA = cv.All(
|
||||
pins.gpio_base_schema(
|
||||
GPIOPin,
|
||||
validate_gpio_pin,
|
||||
modes=pins.GPIO_STANDARD_MODES + (CONF_ANALOG,),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@pins.PIN_SCHEMA_REGISTRY.register("nrf52", NRF52_PIN_SCHEMA)
|
||||
async def nrf52_pin_to_code(config):
|
||||
var = cg.new_Pvariable(config[CONF_ID])
|
||||
num = config[CONF_NUMBER]
|
||||
cg.add(var.set_pin(num))
|
||||
cg.add(var.set_inverted(config[CONF_INVERTED]))
|
||||
cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
|
||||
return var
|
41
esphome/components/nrf52/power.cpp
Normal file
41
esphome/components/nrf52/power.cpp
Normal file
|
@ -0,0 +1,41 @@
|
|||
#ifdef USE_NRF52
|
||||
#include <zephyr/init.h>
|
||||
#include <hal/nrf_power.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace nrf52 {
|
||||
|
||||
static int board_esphome_init(void) {
|
||||
/* if the board is powered from USB
|
||||
* (high voltage mode), GPIO output voltage is set to 1.8 volts by
|
||||
* default and that is not enough to turn the green and blue LEDs on.
|
||||
* Increase GPIO voltage to 3.3 volts.
|
||||
*/
|
||||
if ((nrf_power_mainregstatus_get(NRF_POWER) == NRF_POWER_MAINREGSTATUS_HIGH) &&
|
||||
((NRF_UICR->REGOUT0 & UICR_REGOUT0_VOUT_Msk) == (UICR_REGOUT0_VOUT_DEFAULT << UICR_REGOUT0_VOUT_Pos))) {
|
||||
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Wen << NVMC_CONFIG_WEN_Pos;
|
||||
while (NRF_NVMC->READY == NVMC_READY_READY_Busy) {
|
||||
;
|
||||
}
|
||||
|
||||
NRF_UICR->REGOUT0 =
|
||||
(NRF_UICR->REGOUT0 & ~((uint32_t) UICR_REGOUT0_VOUT_Msk)) | (UICR_REGOUT0_VOUT_3V0 << UICR_REGOUT0_VOUT_Pos);
|
||||
|
||||
NRF_NVMC->CONFIG = NVMC_CONFIG_WEN_Ren << NVMC_CONFIG_WEN_Pos;
|
||||
while (NRF_NVMC->READY == NVMC_READY_READY_Busy) {
|
||||
;
|
||||
}
|
||||
/* a reset is required for changes to take effect */
|
||||
NVIC_SystemReset();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace nrf52
|
||||
} // namespace esphome
|
||||
|
||||
static int board_esphome_init(void) { return esphome::nrf52::board_esphome_init(); }
|
||||
|
||||
SYS_INIT(board_esphome_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
|
||||
|
||||
#endif
|
222
esphome/components/zephyr/__init__.py
Normal file
222
esphome/components/zephyr/__init__.py
Normal file
|
@ -0,0 +1,222 @@
|
|||
import os
|
||||
from typing import Union
|
||||
import esphome.codegen as cg
|
||||
from esphome.core import CORE
|
||||
from esphome.helpers import (
|
||||
write_file_if_changed,
|
||||
copy_file_if_changed,
|
||||
)
|
||||
from esphome.const import (
|
||||
CONF_BOARD,
|
||||
KEY_NAME,
|
||||
)
|
||||
from .const import (
|
||||
KEY_ZEPHYR,
|
||||
KEY_PRJ_CONF,
|
||||
KEY_OVERLAY,
|
||||
zephyr_ns,
|
||||
BOOTLOADER_MCUBOOT,
|
||||
KEY_EXTRA_BUILD_FILES,
|
||||
KEY_PATH,
|
||||
KEY_BOOTLOADER,
|
||||
)
|
||||
|
||||
|
||||
AUTO_LOAD = ["preferences"]
|
||||
KEY_BOARD = "board"
|
||||
|
||||
KEY_USER = "user"
|
||||
|
||||
|
||||
def zephyr_set_core_data(config):
|
||||
CORE.data[KEY_ZEPHYR] = {}
|
||||
CORE.data[KEY_ZEPHYR][KEY_BOARD] = config[CONF_BOARD]
|
||||
CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF] = {}
|
||||
CORE.data[KEY_ZEPHYR][KEY_OVERLAY] = ""
|
||||
CORE.data[KEY_ZEPHYR][KEY_USER] = {}
|
||||
CORE.data[KEY_ZEPHYR][KEY_BOOTLOADER] = config[KEY_BOOTLOADER]
|
||||
CORE.data[KEY_ZEPHYR][KEY_EXTRA_BUILD_FILES] = {}
|
||||
return config
|
||||
|
||||
|
||||
PrjConfValueType = Union[bool, str, int]
|
||||
|
||||
|
||||
def zephyr_add_prj_conf(name: str, value: PrjConfValueType, required: bool = True):
|
||||
"""Set an zephyr prj conf value."""
|
||||
if not name.startswith("CONFIG_"):
|
||||
name = "CONFIG_" + name
|
||||
if name in CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF]:
|
||||
old_value = CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF][name]
|
||||
if old_value[0] != value and old_value[1]:
|
||||
raise ValueError(
|
||||
f"{name} already set with value '{old_value[0]}', cannot set again to '{value}'"
|
||||
)
|
||||
if required:
|
||||
CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF][name] = (value, required)
|
||||
else:
|
||||
CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF][name] = (value, required)
|
||||
|
||||
|
||||
def zephyr_add_user(key, value):
|
||||
if key not in CORE.data[KEY_ZEPHYR][KEY_USER]:
|
||||
CORE.data[KEY_ZEPHYR][KEY_USER][key] = []
|
||||
CORE.data[KEY_ZEPHYR][KEY_USER][key] += [value]
|
||||
|
||||
|
||||
def zephyr_add_overlay(content):
|
||||
CORE.data[KEY_ZEPHYR][KEY_OVERLAY] += content
|
||||
|
||||
|
||||
def add_extra_build_file(filename: str, path: str) -> bool:
|
||||
"""Add an extra build file to the project."""
|
||||
if filename not in CORE.data[KEY_ZEPHYR][KEY_EXTRA_BUILD_FILES]:
|
||||
CORE.data[KEY_ZEPHYR][KEY_EXTRA_BUILD_FILES][filename] = {
|
||||
KEY_NAME: filename,
|
||||
KEY_PATH: path,
|
||||
}
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_extra_script(stage: str, filename: str, path: str):
|
||||
"""Add an extra script to the project."""
|
||||
key = f"{stage}:{filename}"
|
||||
if add_extra_build_file(filename, path):
|
||||
cg.add_platformio_option("extra_scripts", [key])
|
||||
|
||||
|
||||
def zephyr_to_code(conf):
|
||||
cg.add(zephyr_ns.setup_preferences())
|
||||
cg.add_build_flag("-DUSE_ZEPHYR")
|
||||
# build is done by west so bypass board checking in platformio
|
||||
cg.add_platformio_option("boards_dir", CORE.relative_build_path("boards"))
|
||||
|
||||
# c++ support
|
||||
zephyr_add_prj_conf("NEWLIB_LIBC", True)
|
||||
zephyr_add_prj_conf("CONFIG_FPU", True)
|
||||
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
|
||||
zephyr_add_prj_conf("CPLUSPLUS", True)
|
||||
zephyr_add_prj_conf("LIB_CPLUSPLUS", True)
|
||||
# preferences
|
||||
zephyr_add_prj_conf("SETTINGS", True)
|
||||
zephyr_add_prj_conf("NVS", True)
|
||||
zephyr_add_prj_conf("FLASH_MAP", True)
|
||||
zephyr_add_prj_conf("CONFIG_FLASH", True)
|
||||
# watchdog
|
||||
zephyr_add_prj_conf("WATCHDOG", True)
|
||||
zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False)
|
||||
# disable console
|
||||
zephyr_add_prj_conf("UART_CONSOLE", False)
|
||||
zephyr_add_prj_conf("CONSOLE", False, False)
|
||||
# TODO move to nrf52
|
||||
# use NFC pins as GPIO
|
||||
zephyr_add_prj_conf("NFCT_PINS_AS_GPIOS", True)
|
||||
|
||||
add_extra_script(
|
||||
"pre",
|
||||
"pre_build.py",
|
||||
os.path.join(os.path.dirname(__file__), "pre_build.py.script"),
|
||||
)
|
||||
|
||||
|
||||
def _format_prj_conf_val(value: PrjConfValueType) -> str:
|
||||
if isinstance(value, bool):
|
||||
return "y" if value else "n"
|
||||
if isinstance(value, int):
|
||||
return str(value)
|
||||
if isinstance(value, str):
|
||||
return f'"{value}"'
|
||||
raise ValueError
|
||||
|
||||
|
||||
def zephyr_add_cdc_acm(config, id):
|
||||
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
|
||||
zephyr_add_prj_conf("USB_CDC_ACM", True)
|
||||
# prevent device to go to susspend, without this communication stop working in python
|
||||
# there should be a way to solve it
|
||||
zephyr_add_prj_conf("USB_DEVICE_REMOTE_WAKEUP", False)
|
||||
# prevent logging when buffer is full
|
||||
zephyr_add_prj_conf("USB_CDC_ACM_LOG_LEVEL_WRN", True)
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
&zephyr_udc0 {{
|
||||
cdc_acm_uart{id}: cdc_acm_uart{id} {{
|
||||
compatible = "zephyr,cdc-acm-uart";
|
||||
}};
|
||||
}};
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
# Called by writer.py
|
||||
def copy_files():
|
||||
want_opts = CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF]
|
||||
|
||||
prj_conf = (
|
||||
"\n".join(
|
||||
f"{name}={_format_prj_conf_val(value[0])}"
|
||||
for name, value in sorted(want_opts.items())
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), prj_conf)
|
||||
|
||||
if CORE.data[KEY_ZEPHYR][KEY_USER]:
|
||||
zephyr_add_overlay(
|
||||
f"""
|
||||
/ {{
|
||||
zephyr,user {{
|
||||
{[f"{key} = {', '.join(value)};" for key, value in CORE.data[KEY_ZEPHYR][KEY_USER].items()][0]}
|
||||
}};
|
||||
}};"""
|
||||
)
|
||||
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path("zephyr/app.overlay"),
|
||||
CORE.data[KEY_ZEPHYR][KEY_OVERLAY],
|
||||
)
|
||||
|
||||
# write_file_if_changed(
|
||||
# CORE.relative_build_path("zephyr/child_image/mcuboot.conf"),
|
||||
# """
|
||||
# CONFIG_MCUBOOT_SERIAL=y
|
||||
# CONFIG_BOOT_SERIAL_PIN_RESET=y
|
||||
# CONFIG_UART_CONSOLE=n
|
||||
# CONFIG_BOOT_SERIAL_ENTRANCE_GPIO=n
|
||||
# CONFIG_BOOT_SERIAL_CDC_ACM=y
|
||||
# CONFIG_UART_NRFX=n
|
||||
# CONFIG_LOG=n
|
||||
# CONFIG_ASSERT_VERBOSE=n
|
||||
# CONFIG_BOOT_BANNER=n
|
||||
# CONFIG_PRINTK=n
|
||||
# CONFIG_CBPRINTF_LIBC_SUBSTS=n
|
||||
# """,
|
||||
# )
|
||||
|
||||
if CORE.data[KEY_ZEPHYR][KEY_BOOTLOADER] == BOOTLOADER_MCUBOOT:
|
||||
fake_board_manifest = """
|
||||
{
|
||||
"frameworks": [
|
||||
"zephyr"
|
||||
],
|
||||
"name": "esphome nrf52",
|
||||
"upload": {
|
||||
"maximum_ram_size": 248832,
|
||||
"maximum_size": 815104
|
||||
},
|
||||
"url": "https://esphome.io/",
|
||||
"vendor": "esphome"
|
||||
}
|
||||
"""
|
||||
write_file_if_changed(
|
||||
CORE.relative_build_path(f"boards/{CORE.data[KEY_ZEPHYR][KEY_BOARD]}.json"),
|
||||
fake_board_manifest,
|
||||
)
|
||||
|
||||
for _, file in CORE.data[KEY_ZEPHYR][KEY_EXTRA_BUILD_FILES].items():
|
||||
copy_file_if_changed(
|
||||
file[KEY_PATH],
|
||||
CORE.relative_build_path(file[KEY_NAME]),
|
||||
)
|
12
esphome/components/zephyr/const.py
Normal file
12
esphome/components/zephyr/const.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
import esphome.codegen as cg
|
||||
|
||||
KEY_ZEPHYR = "zephyr"
|
||||
KEY_PRJ_CONF = "prj_conf"
|
||||
KEY_OVERLAY = "overlay"
|
||||
KEY_BOOTLOADER = "bootloader"
|
||||
KEY_EXTRA_BUILD_FILES = "extra_build_files"
|
||||
KEY_PATH = "path"
|
||||
|
||||
BOOTLOADER_MCUBOOT = "mcuboot"
|
||||
|
||||
zephyr_ns = cg.esphome_ns.namespace("zephyr")
|
53
esphome/components/zephyr/core.cpp
Normal file
53
esphome/components/zephyr/core.cpp
Normal file
|
@ -0,0 +1,53 @@
|
|||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/drivers/watchdog.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static int wdt_channel_id = -EINVAL;
|
||||
const device *wdt = nullptr;
|
||||
|
||||
void yield() { ::k_yield(); }
|
||||
uint32_t millis() { return k_ticks_to_ms_floor32(k_uptime_ticks()); }
|
||||
void delay(uint32_t ms) { ::k_msleep(ms); }
|
||||
uint32_t micros() { return k_ticks_to_us_floor32(k_uptime_ticks()); }
|
||||
|
||||
void arch_init() {
|
||||
wdt = DEVICE_DT_GET(DT_ALIAS(watchdog0));
|
||||
|
||||
if (device_is_ready(wdt)) {
|
||||
static wdt_timeout_cfg wdt_config{};
|
||||
wdt_config.flags = WDT_FLAG_RESET_SOC;
|
||||
wdt_config.window.max = 2000;
|
||||
wdt_channel_id = wdt_install_timeout(wdt, &wdt_config);
|
||||
if (wdt_channel_id >= 0) {
|
||||
wdt_setup(wdt, WDT_OPT_PAUSE_HALTED_BY_DBG | WDT_OPT_PAUSE_IN_SLEEP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void arch_feed_wdt() {
|
||||
if (wdt_channel_id >= 0) {
|
||||
wdt_feed(wdt, wdt_channel_id);
|
||||
}
|
||||
}
|
||||
|
||||
void arch_restart() { sys_reboot(SYS_REBOOT_COLD); }
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
void setup();
|
||||
void loop();
|
||||
|
||||
int main() {
|
||||
setup();
|
||||
while (1) {
|
||||
loop();
|
||||
esphome::yield();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
120
esphome/components/zephyr/gpio.cpp
Normal file
120
esphome/components/zephyr/gpio.cpp
Normal file
|
@ -0,0 +1,120 @@
|
|||
#ifdef USE_ZEPHYR
|
||||
#include "gpio.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/drivers/gpio.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace zephyr {
|
||||
|
||||
static const char *const TAG = "zephyr";
|
||||
|
||||
static int flags_to_mode(gpio::Flags flags, bool inverted, bool value) {
|
||||
int ret = 0;
|
||||
if (flags & gpio::FLAG_INPUT) {
|
||||
ret |= GPIO_INPUT;
|
||||
}
|
||||
if (flags & gpio::FLAG_OUTPUT) {
|
||||
ret |= GPIO_OUTPUT;
|
||||
if (value != inverted) {
|
||||
ret |= GPIO_OUTPUT_INIT_HIGH;
|
||||
} else {
|
||||
ret |= GPIO_OUTPUT_INIT_LOW;
|
||||
}
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLUP) {
|
||||
ret |= GPIO_PULL_UP;
|
||||
}
|
||||
if (flags & gpio::FLAG_PULLDOWN) {
|
||||
ret |= GPIO_PULL_DOWN;
|
||||
}
|
||||
if (flags & gpio::FLAG_OPEN_DRAIN) {
|
||||
ret |= GPIO_OPEN_DRAIN;
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
struct ISRPinArg {
|
||||
uint8_t pin;
|
||||
bool inverted;
|
||||
};
|
||||
|
||||
ISRInternalGPIOPin ZephyrGPIOPin::to_isr() const {
|
||||
auto *arg = new ISRPinArg{};
|
||||
arg->pin = pin_;
|
||||
arg->inverted = inverted_;
|
||||
return ISRInternalGPIOPin((void *) arg);
|
||||
}
|
||||
|
||||
void ZephyrGPIOPin::attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const {
|
||||
// TODO
|
||||
}
|
||||
|
||||
void ZephyrGPIOPin::setup() {
|
||||
const struct device *gpio = nullptr;
|
||||
if (pin_ < 32) {
|
||||
#define GPIO0 DT_NODELABEL(gpio0)
|
||||
#if DT_NODE_HAS_STATUS(GPIO0, okay)
|
||||
gpio = DEVICE_DT_GET(GPIO0);
|
||||
#else
|
||||
#error "gpio0 is disabled"
|
||||
#endif
|
||||
} else {
|
||||
#define GPIO1 DT_NODELABEL(gpio1)
|
||||
#if DT_NODE_HAS_STATUS(GPIO1, okay)
|
||||
gpio = DEVICE_DT_GET(GPIO1);
|
||||
#else
|
||||
#error "gpio1 is disabled"
|
||||
#endif
|
||||
}
|
||||
if (device_is_ready(gpio)) {
|
||||
gpio_ = gpio;
|
||||
} else {
|
||||
ESP_LOGE(TAG, "gpio %u is not ready.", pin_);
|
||||
return;
|
||||
}
|
||||
pin_mode(flags_);
|
||||
}
|
||||
|
||||
void ZephyrGPIOPin::pin_mode(gpio::Flags flags) {
|
||||
if (nullptr == gpio_) {
|
||||
return;
|
||||
}
|
||||
gpio_pin_configure(gpio_, pin_ % 32, flags_to_mode(flags, inverted_, value_));
|
||||
}
|
||||
|
||||
std::string ZephyrGPIOPin::dump_summary() const {
|
||||
char buffer[32];
|
||||
snprintf(buffer, sizeof(buffer), "GPIO%u", pin_);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
bool ZephyrGPIOPin::digital_read() {
|
||||
if (nullptr == gpio_) {
|
||||
return false;
|
||||
}
|
||||
return bool(gpio_pin_get(gpio_, pin_ % 32) != inverted_);
|
||||
}
|
||||
|
||||
void ZephyrGPIOPin::digital_write(bool value) {
|
||||
// make sure that value is not ignored since it can be inverted e.g. on switch side
|
||||
// that way init state should be correct
|
||||
value_ = value;
|
||||
if (nullptr == gpio_) {
|
||||
return;
|
||||
}
|
||||
gpio_pin_set(gpio_, pin_ % 32, value != inverted_ ? 1 : 0);
|
||||
}
|
||||
void ZephyrGPIOPin::detach_interrupt() const {
|
||||
// TODO
|
||||
}
|
||||
|
||||
} // namespace zephyr
|
||||
|
||||
bool IRAM_ATTR ISRInternalGPIOPin::digital_read() {
|
||||
// TODO
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
37
esphome/components/zephyr/gpio.h
Normal file
37
esphome/components/zephyr/gpio.h
Normal file
|
@ -0,0 +1,37 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include "esphome/core/hal.h"
|
||||
struct device;
|
||||
namespace esphome {
|
||||
namespace zephyr {
|
||||
|
||||
class ZephyrGPIOPin : public InternalGPIOPin {
|
||||
public:
|
||||
void set_pin(uint8_t pin) { pin_ = pin; }
|
||||
void set_inverted(bool inverted) { inverted_ = inverted; }
|
||||
void set_flags(gpio::Flags flags) { flags_ = flags; }
|
||||
|
||||
void setup() override;
|
||||
void pin_mode(gpio::Flags flags) override;
|
||||
bool digital_read() override;
|
||||
void digital_write(bool value) override;
|
||||
std::string dump_summary() const override;
|
||||
void detach_interrupt() const override;
|
||||
ISRInternalGPIOPin to_isr() const override;
|
||||
uint8_t get_pin() const override { return pin_; }
|
||||
bool is_inverted() const override { return inverted_; }
|
||||
|
||||
protected:
|
||||
void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
|
||||
uint8_t pin_;
|
||||
bool inverted_;
|
||||
gpio::Flags flags_;
|
||||
const device *gpio_ = nullptr;
|
||||
bool value_ = false;
|
||||
};
|
||||
|
||||
} // namespace zephyr
|
||||
} // namespace esphome
|
||||
|
||||
#endif // USE_ZEPHYR
|
4
esphome/components/zephyr/pre_build.py.script
Normal file
4
esphome/components/zephyr/pre_build.py.script
Normal file
|
@ -0,0 +1,4 @@
|
|||
Import("env")
|
||||
|
||||
board_config = env.BoardConfig()
|
||||
board_config.update("frameworks", ["arduino", "zephyr"])
|
155
esphome/components/zephyr/preferences.cpp
Normal file
155
esphome/components/zephyr/preferences.cpp
Normal file
|
@ -0,0 +1,155 @@
|
|||
#ifdef USE_ZEPHYR
|
||||
|
||||
#include "esphome/core/preferences.h"
|
||||
#include "esphome/core/log.h"
|
||||
#include <zephyr/settings/settings.h>
|
||||
|
||||
namespace esphome {
|
||||
namespace zephyr {
|
||||
|
||||
static const char *const TAG = "zephyr.preferences";
|
||||
|
||||
#define ESPHOME_SETTINGS_KEY "esphome"
|
||||
|
||||
class ZephyrPreferenceBackend : public ESPPreferenceBackend {
|
||||
public:
|
||||
ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; }
|
||||
ZephyrPreferenceBackend(uint32_t type, std::vector<uint8_t> &&data) : data(std::move(data)) { this->type_ = type; }
|
||||
|
||||
bool save(const uint8_t *data, size_t len) override {
|
||||
this->data.resize(len);
|
||||
std::memcpy(this->data.data(), data, len);
|
||||
ESP_LOGVV(TAG, "save key: %u, len: %d", type_, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool load(uint8_t *data, size_t len) override {
|
||||
if (len != this->data.size()) {
|
||||
ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len);
|
||||
return false;
|
||||
}
|
||||
std::memcpy(data, this->data.data(), len);
|
||||
ESP_LOGVV(TAG, "load key: %u, len: %d", type_, len);
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint32_t get_type() const { return type_; }
|
||||
const std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, type_); }
|
||||
|
||||
std::vector<uint8_t> data;
|
||||
|
||||
protected:
|
||||
uint32_t type_ = 0;
|
||||
};
|
||||
|
||||
class ZephyrPreferences : public ESPPreferences {
|
||||
public:
|
||||
void open() {
|
||||
int err = settings_subsys_init();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Failed to initialize settings subsystem, err: %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
static struct settings_handler settings_cb = {
|
||||
.name = ESPHOME_SETTINGS_KEY,
|
||||
.h_set = load_setting_,
|
||||
.h_export = export_settings_,
|
||||
};
|
||||
|
||||
err = settings_register(&settings_cb);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "setting_register failed, err, %d", err);
|
||||
return;
|
||||
}
|
||||
|
||||
err = settings_load_subtree(ESPHOME_SETTINGS_KEY);
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Cannot load settings, err: %d", err);
|
||||
return;
|
||||
}
|
||||
ESP_LOGD(TAG, "Loaded %u settings.", backends_.size());
|
||||
}
|
||||
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type, bool in_flash) override {
|
||||
return make_preference(length, type);
|
||||
}
|
||||
|
||||
ESPPreferenceObject make_preference(size_t length, uint32_t type) override {
|
||||
for (auto backend : backends_) {
|
||||
if (backend->get_type() == type) {
|
||||
return ESPPreferenceObject(backend);
|
||||
}
|
||||
}
|
||||
printf("type %u size %u\n", type, backends_.size());
|
||||
auto *pref = new ZephyrPreferenceBackend(type);
|
||||
ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str());
|
||||
backends_.push_back(pref);
|
||||
return ESPPreferenceObject(pref);
|
||||
}
|
||||
|
||||
bool sync() override {
|
||||
ESP_LOGD(TAG, "Save settings");
|
||||
int err = settings_save();
|
||||
if (err) {
|
||||
ESP_LOGE(TAG, "Cannot save settings, err: %d", err);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool reset() override {
|
||||
ESP_LOGD(TAG, "Reset settings");
|
||||
for (auto backend : backends_) {
|
||||
// save empty delete data
|
||||
backend->data.clear();
|
||||
}
|
||||
sync();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::vector<ZephyrPreferenceBackend *> backends_;
|
||||
|
||||
static int load_setting_(const char *name, size_t len, settings_read_cb read_cb, void *cb_arg) {
|
||||
auto type = parse_hex<uint32_t>(name);
|
||||
if (!type.has_value()) {
|
||||
std::string full_name(ESPHOME_SETTINGS_KEY);
|
||||
full_name += "/";
|
||||
full_name += name;
|
||||
// Delete unusable keys. Otherwise it will stay in flash forever.
|
||||
settings_delete(full_name.c_str());
|
||||
return 1;
|
||||
}
|
||||
std::vector<uint8_t> data(len);
|
||||
int err = read_cb(cb_arg, data.data(), len);
|
||||
|
||||
ESP_LOGD(TAG, "load setting, name: %s(%u), len %u, err %u", name, *type, len, err);
|
||||
auto *pref = new ZephyrPreferenceBackend(*type, std::move(data));
|
||||
static_cast<ZephyrPreferences *>(global_preferences)->backends_.push_back(pref);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int export_settings_(int (*cb)(const char *name, const void *value, size_t val_len)) {
|
||||
for (auto backend : static_cast<ZephyrPreferences *>(global_preferences)->backends_) {
|
||||
auto name = backend->get_key();
|
||||
int err = cb(name.c_str(), backend->data.data(), backend->data.size());
|
||||
ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
void setup_preferences() {
|
||||
auto *prefs = new ZephyrPreferences();
|
||||
global_preferences = prefs;
|
||||
prefs->open();
|
||||
}
|
||||
|
||||
} // namespace zephyr
|
||||
|
||||
ESPPreferences *global_preferences; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
13
esphome/components/zephyr/preferences.h
Normal file
13
esphome/components/zephyr/preferences.h
Normal file
|
@ -0,0 +1,13 @@
|
|||
#pragma once
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
|
||||
namespace esphome {
|
||||
namespace zephyr {
|
||||
|
||||
void setup_preferences();
|
||||
|
||||
} // namespace zephyr
|
||||
} // namespace esphome
|
||||
|
||||
#endif
|
|
@ -62,6 +62,7 @@ from esphome.const import (
|
|||
TYPE_LOCAL,
|
||||
VALID_SUBSTITUTIONS_CHARACTERS,
|
||||
__version__ as ESPHOME_VERSION,
|
||||
PLATFORM_NRF52,
|
||||
)
|
||||
from esphome.core import (
|
||||
CORE,
|
||||
|
@ -604,8 +605,10 @@ def only_with_framework(frameworks):
|
|||
only_on_esp32 = only_on(PLATFORM_ESP32)
|
||||
only_on_esp8266 = only_on(PLATFORM_ESP8266)
|
||||
only_on_rp2040 = only_on(PLATFORM_RP2040)
|
||||
only_on_nrf52 = only_on(PLATFORM_NRF52)
|
||||
only_with_arduino = only_with_framework("arduino")
|
||||
only_with_esp_idf = only_with_framework("esp-idf")
|
||||
only_with_zephyr = only_with_framework("zephyr")
|
||||
|
||||
|
||||
# Adapted from:
|
||||
|
@ -1648,6 +1651,7 @@ class SplitDefault(Optional):
|
|||
bk72xx=vol.UNDEFINED,
|
||||
rtl87xx=vol.UNDEFINED,
|
||||
host=vol.UNDEFINED,
|
||||
nrf52=vol.UNDEFINED,
|
||||
):
|
||||
super().__init__(key)
|
||||
self._esp8266_default = vol.default_factory(esp8266)
|
||||
|
@ -1679,6 +1683,7 @@ class SplitDefault(Optional):
|
|||
self._bk72xx_default = vol.default_factory(bk72xx)
|
||||
self._rtl87xx_default = vol.default_factory(rtl87xx)
|
||||
self._host_default = vol.default_factory(host)
|
||||
self._nrf52_default = vol.default_factory(nrf52)
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
|
@ -1721,6 +1726,8 @@ class SplitDefault(Optional):
|
|||
return self._rtl87xx_default
|
||||
if CORE.is_host:
|
||||
return self._host_default
|
||||
if CORE.is_nrf52:
|
||||
return self._nrf52_default
|
||||
raise NotImplementedError
|
||||
|
||||
@default.setter
|
||||
|
|
|
@ -14,6 +14,7 @@ PLATFORM_HOST = "host"
|
|||
PLATFORM_BK72XX = "bk72xx"
|
||||
PLATFORM_RTL87XX = "rtl87xx"
|
||||
PLATFORM_LIBRETINY_OLDSTYLE = "libretiny"
|
||||
PLATFORM_NRF52 = "nrf52"
|
||||
|
||||
TARGET_PLATFORMS = [
|
||||
PLATFORM_ESP32,
|
||||
|
@ -23,6 +24,7 @@ TARGET_PLATFORMS = [
|
|||
PLATFORM_BK72XX,
|
||||
PLATFORM_RTL87XX,
|
||||
PLATFORM_LIBRETINY_OLDSTYLE,
|
||||
PLATFORM_NRF52,
|
||||
]
|
||||
|
||||
SOURCE_FILE_EXTENSIONS = {".cpp", ".hpp", ".h", ".c", ".tcc", ".ino"}
|
||||
|
|
|
@ -21,6 +21,7 @@ from esphome.const import (
|
|||
PLATFORM_RTL87XX,
|
||||
PLATFORM_RP2040,
|
||||
PLATFORM_HOST,
|
||||
PLATFORM_NRF52,
|
||||
)
|
||||
from esphome.coroutine import FakeAwaitable as _FakeAwaitable
|
||||
from esphome.coroutine import FakeEventLoop as _FakeEventLoop
|
||||
|
@ -661,6 +662,10 @@ class EsphomeCore:
|
|||
def is_host(self):
|
||||
return self.target_platform == PLATFORM_HOST
|
||||
|
||||
@property
|
||||
def is_nrf52(self):
|
||||
return self.target_platform == PLATFORM_NRF52
|
||||
|
||||
@property
|
||||
def target_framework(self):
|
||||
return self.data[KEY_CORE][KEY_TARGET_FRAMEWORK]
|
||||
|
@ -673,6 +678,10 @@ class EsphomeCore:
|
|||
def using_esp_idf(self):
|
||||
return self.target_framework == "esp-idf"
|
||||
|
||||
@property
|
||||
def using_zephyr(self):
|
||||
return self.target_framework == "zephyr"
|
||||
|
||||
def add_job(self, func, *args, **kwargs):
|
||||
self.event_loop.add_job(func, *args, **kwargs)
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ from esphome.const import (
|
|||
)
|
||||
from esphome.core import CORE, coroutine_with_priority
|
||||
from esphome.helpers import copy_file_if_changed, get_str_env, walk_files
|
||||
from esphome.components.zephyr import zephyr_add_prj_conf
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
@ -361,6 +362,9 @@ async def to_code(config):
|
|||
)
|
||||
)
|
||||
|
||||
if CORE.using_zephyr:
|
||||
zephyr_add_prj_conf("BT_DEVICE_NAME", config[CONF_NAME])
|
||||
|
||||
CORE.add_job(_add_automations, config)
|
||||
|
||||
cg.add_build_flag("-fno-exceptions")
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
#include <cstdarg>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <strings.h>
|
||||
|
||||
#ifdef USE_HOST
|
||||
#ifndef _WIN32
|
||||
|
@ -55,6 +56,10 @@
|
|||
#include <WiFi.h> // for macAddress()
|
||||
#endif
|
||||
|
||||
#ifdef USE_ZEPHYR
|
||||
#include <zephyr/random/rand32.h>
|
||||
#endif
|
||||
|
||||
namespace esphome {
|
||||
|
||||
static const char *const TAG = "helpers";
|
||||
|
@ -209,6 +214,8 @@ uint32_t random_uint32() {
|
|||
std::mt19937 rng(dev());
|
||||
std::uniform_int_distribution<uint32_t> dist(0, std::numeric_limits<uint32_t>::max());
|
||||
return dist(rng);
|
||||
#elif defined(USE_ZEPHYR)
|
||||
return rand();
|
||||
#else
|
||||
#error "No random source available for this configuration."
|
||||
#endif
|
||||
|
@ -246,6 +253,9 @@ bool random_bytes(uint8_t *data, size_t len) {
|
|||
}
|
||||
fclose(fp);
|
||||
return true;
|
||||
#elif defined(USE_ZEPHYR)
|
||||
sys_rand_get(data, len);
|
||||
return true;
|
||||
#else
|
||||
#error "No random source available for this configuration."
|
||||
#endif
|
||||
|
@ -624,6 +634,11 @@ Mutex::Mutex() {}
|
|||
void Mutex::lock() {}
|
||||
bool Mutex::try_lock() { return true; }
|
||||
void Mutex::unlock() {}
|
||||
#elif defined(USE_ZEPHYR)
|
||||
Mutex::Mutex() { k_mutex_init(&handle_); }
|
||||
void Mutex::lock() { k_mutex_lock(&this->handle_, K_FOREVER); }
|
||||
bool Mutex::try_lock() { return k_mutex_lock(&this->handle_, K_NO_WAIT) == 0; }
|
||||
void Mutex::unlock() { k_mutex_unlock(&this->handle_); }
|
||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
|
||||
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
|
||||
|
@ -681,6 +696,13 @@ void get_mac_address_raw(uint8_t *mac) { // NOLINT(readability-non-const-parame
|
|||
WiFi.macAddress(mac);
|
||||
#elif defined(USE_LIBRETINY)
|
||||
WiFi.macAddress(mac);
|
||||
#elif defined(USE_NRF52)
|
||||
mac[0] = ((NRF_FICR->DEVICEADDR[1] & 0xFFFF) >> 8) | 0xC0;
|
||||
mac[1] = NRF_FICR->DEVICEADDR[1] & 0xFFFF;
|
||||
mac[2] = NRF_FICR->DEVICEADDR[0] >> 24;
|
||||
mac[3] = NRF_FICR->DEVICEADDR[0] >> 16;
|
||||
mac[4] = NRF_FICR->DEVICEADDR[0] >> 8;
|
||||
mac[5] = NRF_FICR->DEVICEADDR[0];
|
||||
#else
|
||||
// this should be an error, but that messes with CI checks. #error No mac address method defined
|
||||
#endif
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
#include <limits>
|
||||
#include <array>
|
||||
|
||||
#include "esphome/core/optional.h"
|
||||
|
||||
|
@ -20,6 +22,8 @@
|
|||
#elif defined(USE_LIBRETINY)
|
||||
#include <FreeRTOS.h>
|
||||
#include <semphr.h>
|
||||
#elif defined(USE_ZEPHYR)
|
||||
#include <zephyr/kernel.h>
|
||||
#endif
|
||||
|
||||
#define HOT __attribute__((hot))
|
||||
|
@ -552,7 +556,9 @@ class Mutex {
|
|||
Mutex &operator=(const Mutex &) = delete;
|
||||
|
||||
private:
|
||||
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
#if defined(USE_ZEPHYR)
|
||||
k_mutex handle_;
|
||||
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
|
||||
SemaphoreHandle_t handle_;
|
||||
#endif
|
||||
};
|
||||
|
|
|
@ -310,6 +310,10 @@ def copy_src_tree():
|
|||
CORE.relative_src_path("esphome.h"),
|
||||
ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'),
|
||||
)
|
||||
elif CORE.using_zephyr:
|
||||
from esphome.components.zephyr import copy_files
|
||||
|
||||
copy_files()
|
||||
|
||||
|
||||
def generate_defines_h():
|
||||
|
|
173
esphome/zephyr_tools.py
Normal file
173
esphome/zephyr_tools.py
Normal file
|
@ -0,0 +1,173 @@
|
|||
import time
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
from typing import Final
|
||||
from rich.pretty import pprint
|
||||
from bleak import BleakScanner, BleakClient
|
||||
from bleak.exc import BleakDeviceNotFoundError, BleakDBusError
|
||||
from smpclient.transport.ble import SMPBLETransport
|
||||
from smpclient.transport import SMPTransportDisconnected
|
||||
from smpclient.transport.serial import SMPSerialTransport
|
||||
from smpclient import SMPClient
|
||||
from smpclient.mcuboot import IMAGE_TLV, ImageInfo, TLVNotFound, MCUBootImageError
|
||||
from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite
|
||||
from smpclient.requests.os_management import ResetWrite
|
||||
from smpclient.generics import error, success
|
||||
from smp.exceptions import SMPBadStartDelimiter
|
||||
from esphome.espota2 import ProgressBar
|
||||
|
||||
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
|
||||
NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E"
|
||||
MAC_ADDRESS_PATTERN: Final = re.compile(
|
||||
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def is_mac_address(value):
|
||||
return MAC_ADDRESS_PATTERN.match(value)
|
||||
|
||||
|
||||
async def logger_scan(name):
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
device = await BleakScanner.find_device_by_name(name)
|
||||
return device
|
||||
|
||||
|
||||
async def logger_connect(host):
|
||||
disconnected_event = asyncio.Event()
|
||||
|
||||
def handle_disconnect(client):
|
||||
disconnected_event.set()
|
||||
|
||||
def handle_rx(_, data: bytearray):
|
||||
print(data.decode("utf-8"), end="")
|
||||
|
||||
_LOGGER.info("Connecting %s...", host)
|
||||
async with BleakClient(host, disconnected_callback=handle_disconnect) as client:
|
||||
_LOGGER.info("Connected %s...", host)
|
||||
try:
|
||||
await client.start_notify(NUS_TX_CHAR_UUID, handle_rx)
|
||||
except BleakDBusError as e:
|
||||
_LOGGER.error("Bluetooth LE logger: %s", e)
|
||||
disconnected_event.set()
|
||||
await disconnected_event.wait()
|
||||
|
||||
|
||||
async def smpmgr_scan(name):
|
||||
_LOGGER.info("Scanning bluetooth for %s...", name)
|
||||
devices = []
|
||||
for device in await BleakScanner.discover(service_uuids=[SMP_SERVICE_UUID]):
|
||||
if device.name == name:
|
||||
devices += [device]
|
||||
return devices
|
||||
|
||||
|
||||
def get_image_tlv_sha256(file):
|
||||
_LOGGER.info("Checking image: %s", str(file))
|
||||
try:
|
||||
image_info = ImageInfo.load_file(str(file))
|
||||
pprint(image_info.header)
|
||||
_LOGGER.debug(str(image_info))
|
||||
except MCUBootImageError as e:
|
||||
_LOGGER.error("Inspection of FW image failed: %s", e)
|
||||
return None
|
||||
|
||||
try:
|
||||
image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256)
|
||||
_LOGGER.debug("IMAGE_TLV_SHA256: %s", image_tlv_sha256)
|
||||
except TLVNotFound:
|
||||
_LOGGER.error("Could not find IMAGE_TLV_SHA256 in image.")
|
||||
return None
|
||||
return image_tlv_sha256.value
|
||||
|
||||
|
||||
async def smpmgr_upload(config, host, firmware):
|
||||
try:
|
||||
return await smpmgr_upload_(config, host, firmware)
|
||||
except SMPTransportDisconnected:
|
||||
_LOGGER.error("%s was disconnected.", host)
|
||||
return 1
|
||||
|
||||
|
||||
async def smpmgr_upload_(config, host, firmware):
|
||||
image_tlv_sha256 = get_image_tlv_sha256(firmware)
|
||||
if image_tlv_sha256 is None:
|
||||
return 1
|
||||
|
||||
if is_mac_address(host):
|
||||
smp_client = SMPClient(SMPBLETransport(), host)
|
||||
else:
|
||||
smp_client = SMPClient(SMPSerialTransport(), host)
|
||||
|
||||
_LOGGER.info("Connecting %s...", host)
|
||||
try:
|
||||
await smp_client.connect()
|
||||
except BleakDeviceNotFoundError:
|
||||
_LOGGER.error("Device %s not found", host)
|
||||
return 1
|
||||
|
||||
_LOGGER.info("Connected %s...", host)
|
||||
|
||||
try:
|
||||
image_state = await smp_client.request(ImageStatesRead(), 2.5)
|
||||
except SMPBadStartDelimiter as e:
|
||||
_LOGGER.error("mcumgr is not supported by device (%s)", e)
|
||||
return 1
|
||||
|
||||
already_uploaded = False
|
||||
|
||||
if error(image_state):
|
||||
_LOGGER.error(image_state)
|
||||
return 1
|
||||
if success(image_state):
|
||||
if len(image_state.images) == 0:
|
||||
_LOGGER.warning("No images on device!")
|
||||
for image in image_state.images:
|
||||
pprint(image)
|
||||
if image.active and not image.confirmed:
|
||||
_LOGGER.error("No free slot")
|
||||
return 1
|
||||
if image.hash == image_tlv_sha256:
|
||||
if already_uploaded:
|
||||
_LOGGER.error("Both slots have the same image")
|
||||
return 1
|
||||
if image.confirmed:
|
||||
_LOGGER.error("Image already confirmted")
|
||||
return 1
|
||||
_LOGGER.warning("The same image already uploaded")
|
||||
already_uploaded = True
|
||||
|
||||
if not already_uploaded:
|
||||
with open(firmware, "rb") as file:
|
||||
image = file.read()
|
||||
file.close()
|
||||
upload_size = len(image)
|
||||
progress = ProgressBar()
|
||||
progress.update(0)
|
||||
try:
|
||||
async for offset in smp_client.upload(image):
|
||||
progress.update(offset / upload_size)
|
||||
finally:
|
||||
progress.done()
|
||||
|
||||
_LOGGER.info("Mark image for testing")
|
||||
r = await smp_client.request(ImageStatesWrite(hash=image_tlv_sha256), 1.0)
|
||||
|
||||
if error(r):
|
||||
_LOGGER.error(r)
|
||||
return 1
|
||||
|
||||
# give a chance to execute completion callback
|
||||
time.sleep(1)
|
||||
_LOGGER.info("Reset")
|
||||
r = await smp_client.request(ResetWrite(), 1.0)
|
||||
|
||||
if error(r):
|
||||
_LOGGER.error(r)
|
||||
return 1
|
||||
|
||||
return 0
|
|
@ -27,3 +27,7 @@ pyparsing >= 3.0
|
|||
|
||||
# For autocompletion
|
||||
argcomplete>=2.0.0
|
||||
|
||||
# for mcumgr
|
||||
rich==13.7.0
|
||||
smpclient==3.2.0
|
||||
|
|
|
@ -540,6 +540,7 @@ def lint_relative_py_import(fname):
|
|||
"esphome/components/rp2040/core.cpp",
|
||||
"esphome/components/libretiny/core.cpp",
|
||||
"esphome/components/host/core.cpp",
|
||||
"esphome/components/zephyr/core.cpp",
|
||||
],
|
||||
)
|
||||
def lint_namespace(fname, content):
|
||||
|
|
14
tests/components/gpio/test.nrf52-adafruit.yaml
Normal file
14
tests/components/gpio/test.nrf52-adafruit.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 2
|
||||
id: gpio_binary_sensor
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: 3
|
||||
id: gpio_output
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
id: gpio_switch
|
14
tests/components/gpio/test.nrf52-mcumgr.yaml
Normal file
14
tests/components/gpio/test.nrf52-mcumgr.yaml
Normal file
|
@ -0,0 +1,14 @@
|
|||
binary_sensor:
|
||||
- platform: gpio
|
||||
pin: 2
|
||||
id: gpio_binary_sensor
|
||||
|
||||
output:
|
||||
- platform: gpio
|
||||
pin: 3
|
||||
id: gpio_output
|
||||
|
||||
switch:
|
||||
- platform: gpio
|
||||
pin: 4
|
||||
id: gpio_switch
|
|
@ -0,0 +1,15 @@
|
|||
esphome:
|
||||
name: componenttestnrf52
|
||||
friendly_name: $component_name
|
||||
|
||||
nrf52:
|
||||
board: adafruit_itsybitsy_nrf52840
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
|
@ -0,0 +1,15 @@
|
|||
esphome:
|
||||
name: componenttestnrf52
|
||||
friendly_name: $component_name
|
||||
|
||||
nrf52:
|
||||
board: adafruit_feather_nrf52840
|
||||
|
||||
logger:
|
||||
level: VERY_VERBOSE
|
||||
|
||||
packages:
|
||||
component_under_test: !include
|
||||
file: $component_test_file
|
||||
vars:
|
||||
component_test_file: $component_test_file
|
Loading…
Reference in a new issue