mirror of
https://github.com/esphome/esphome.git
synced 2025-10-30 06:33:51 +00:00
add fota
This commit is contained in:
@@ -6,6 +6,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
import argcomplete
|
import argcomplete
|
||||||
@@ -21,6 +22,7 @@ from esphome.const import (
|
|||||||
CONF_LOGGER,
|
CONF_LOGGER,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_OTA,
|
CONF_OTA,
|
||||||
|
CONF_FOTA,
|
||||||
CONF_MQTT,
|
CONF_MQTT,
|
||||||
CONF_MDNS,
|
CONF_MDNS,
|
||||||
CONF_DISABLED,
|
CONF_DISABLED,
|
||||||
@@ -47,6 +49,7 @@ from esphome.util import (
|
|||||||
get_serial_ports,
|
get_serial_ports,
|
||||||
)
|
)
|
||||||
from esphome.log import color, setup_log, Fore
|
from esphome.log import color, setup_log, Fore
|
||||||
|
from .zephyr_tools import smpmgr_scan, smpmgr_upload, list_pyocd
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -89,12 +92,40 @@ def choose_upload_log_host(
|
|||||||
options = []
|
options = []
|
||||||
for port in get_serial_ports():
|
for port in get_serial_ports():
|
||||||
options.append((f"{port.path} ({port.description})", port.path))
|
options.append((f"{port.path} ({port.description})", port.path))
|
||||||
|
if show_ota and CONF_FOTA in CORE.config:
|
||||||
|
options.append(
|
||||||
|
(f"mcumgr {port.path} ({port.description})", f"mcumgr {port.path}")
|
||||||
|
)
|
||||||
if default == "SERIAL":
|
if default == "SERIAL":
|
||||||
return choose_prompt(options, purpose=purpose)
|
return choose_prompt(options, purpose=purpose)
|
||||||
|
pyocd = list_pyocd()
|
||||||
|
if len(pyocd) > 0:
|
||||||
|
for probe in list_pyocd():
|
||||||
|
options.append(
|
||||||
|
(
|
||||||
|
f"pyOCD {probe['product_name']} ({probe['unique_id']})",
|
||||||
|
f"pyocd {probe['unique_id']}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if default == "PYOCD":
|
||||||
|
return choose_prompt(options, purpose=purpose)
|
||||||
if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
|
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))
|
options.append((f"Over The Air ({CORE.address})", CORE.address))
|
||||||
if default == "OTA":
|
if default == "OTA":
|
||||||
return CORE.address
|
return CORE.address
|
||||||
|
if show_ota and CONF_FOTA in CORE.config:
|
||||||
|
if default is None or default == "FOTA":
|
||||||
|
ble_devices = asyncio.run(smpmgr_scan())
|
||||||
|
if len(ble_devices) == 0:
|
||||||
|
_LOGGER.warning("No FOTA service found!")
|
||||||
|
for device in ble_devices:
|
||||||
|
options.append(
|
||||||
|
(
|
||||||
|
f"FOTA over Bluetooth LE({device.address}) {device.name}",
|
||||||
|
f"mcumgr {device.address}",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return choose_prompt(options, purpose=purpose)
|
||||||
if show_mqtt and CONF_MQTT in CORE.config:
|
if show_mqtt and CONF_MQTT in CORE.config:
|
||||||
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
|
options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT"))
|
||||||
if default == "OTA":
|
if default == "OTA":
|
||||||
@@ -316,6 +347,17 @@ def upload_program(config, args, host):
|
|||||||
|
|
||||||
raise EsphomeError(f"Unknown target platform: {CORE.target_platform}")
|
raise EsphomeError(f"Unknown target platform: {CORE.target_platform}")
|
||||||
|
|
||||||
|
if host.startswith("pyocd"):
|
||||||
|
print(host.split(" ")[1])
|
||||||
|
raise EsphomeError("Not implemented")
|
||||||
|
|
||||||
|
if host.startswith("mcumgr"):
|
||||||
|
firmware = os.path.abspath(
|
||||||
|
CORE.relative_pioenvs_path(CORE.name, "zephyr", "app_update.bin")
|
||||||
|
)
|
||||||
|
if CONF_FOTA in config:
|
||||||
|
return asyncio.run(smpmgr_upload(config, host.split(" ")[1], firmware))
|
||||||
|
|
||||||
if CONF_OTA not in config:
|
if CONF_OTA not in config:
|
||||||
raise EsphomeError(
|
raise EsphomeError(
|
||||||
"Cannot upload Over the Air as the config does not include the ota: "
|
"Cannot upload Over the Air as the config does not include the ota: "
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from esphome.core import CORE
|
from esphome.core import CORE
|
||||||
from esphome.components.nrf52 import add_zephyr_prj_conf_option
|
from esphome.components.nrf52.zephyr import zephyr_add_prj_conf
|
||||||
|
|
||||||
dfu_ns = cg.esphome_ns.namespace("dfu")
|
dfu_ns = cg.esphome_ns.namespace("dfu")
|
||||||
DeviceFirmwareUpdate = dfu_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
DeviceFirmwareUpdate = dfu_ns.class_("DeviceFirmwareUpdate", cg.Component)
|
||||||
@@ -32,5 +32,5 @@ async def to_code(config):
|
|||||||
# week symbol do not work for some reason so use wrap instaed
|
# week symbol do not work for some reason so use wrap instaed
|
||||||
cg.add_build_flag("-Wl,--wrap=tud_cdc_line_state_cb")
|
cg.add_build_flag("-Wl,--wrap=tud_cdc_line_state_cb")
|
||||||
elif CORE.using_zephyr:
|
elif CORE.using_zephyr:
|
||||||
add_zephyr_prj_conf_option("CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
|
zephyr_add_prj_conf("CDC_ACM_DTE_RATE_CALLBACK_SUPPORT", True)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
|
|||||||
88
esphome/components/fota/__init__.py
Normal file
88
esphome/components/fota/__init__.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
import esphome.config_validation as cv
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
from esphome.components.nrf52.zephyr import zephyr_add_prj_conf, zephyr_add_overlay
|
||||||
|
|
||||||
|
fota_ns = cg.esphome_ns.namespace("fota")
|
||||||
|
FOTAComponent = fota_ns.class_("FOTAComponent", cg.Component)
|
||||||
|
|
||||||
|
CONFIG_SCHEMA = cv.All(
|
||||||
|
cv.Schema(
|
||||||
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(FOTAComponent),
|
||||||
|
}
|
||||||
|
).extend(cv.COMPONENT_SCHEMA),
|
||||||
|
cv.only_on_nrf52,
|
||||||
|
cv.only_with_zephyr,
|
||||||
|
)
|
||||||
|
|
||||||
|
bt = True
|
||||||
|
cdc = True
|
||||||
|
|
||||||
|
|
||||||
|
async def to_code(config):
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
zephyr_add_prj_conf("NET_BUF", True)
|
||||||
|
zephyr_add_prj_conf("ZCBOR", True)
|
||||||
|
zephyr_add_prj_conf("MCUMGR", True)
|
||||||
|
zephyr_add_prj_conf("BOOTLOADER_MCUBOOT", True)
|
||||||
|
|
||||||
|
zephyr_add_prj_conf("IMG_MANAGER", True)
|
||||||
|
zephyr_add_prj_conf("STREAM_FLASH", True)
|
||||||
|
zephyr_add_prj_conf("FLASH_MAP", True)
|
||||||
|
zephyr_add_prj_conf("FLASH", True)
|
||||||
|
zephyr_add_prj_conf("MCUBOOT_SHELL", True)
|
||||||
|
zephyr_add_prj_conf("SHELL", True)
|
||||||
|
zephyr_add_prj_conf("SYSTEM_WORKQUEUE_STACK_SIZE", 2304)
|
||||||
|
zephyr_add_prj_conf("MAIN_STACK_SIZE", 2048)
|
||||||
|
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_IMG", True)
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_OS", True)
|
||||||
|
|
||||||
|
# echo
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_OS_ECHO", True)
|
||||||
|
# for Android-nRF-Connect-Device-Manager
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_OS_INFO", True)
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_OS_BOOTLOADER_INFO", True)
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_OS_MCUMGR_PARAMS", True)
|
||||||
|
# bt update fails without this
|
||||||
|
zephyr_add_prj_conf("MCUMGR_GRP_SHELL", True)
|
||||||
|
# make MTU bigger and other things
|
||||||
|
zephyr_add_prj_conf("NCS_SAMPLE_MCUMGR_BT_OTA_DFU_SPEEDUP", True)
|
||||||
|
|
||||||
|
# zephyr_add_prj_conf("MCUMGR_TRANSPORT_LOG_LEVEL_DBG", True)
|
||||||
|
|
||||||
|
if bt:
|
||||||
|
zephyr_add_prj_conf("BT", True)
|
||||||
|
zephyr_add_prj_conf("BT_PERIPHERAL", True)
|
||||||
|
zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT", True)
|
||||||
|
# fix corrupted data during ble transport
|
||||||
|
zephyr_add_prj_conf("MCUMGR_TRANSPORT_BT_REASSEMBLY", True)
|
||||||
|
if cdc:
|
||||||
|
zephyr_add_prj_conf("MCUMGR_TRANSPORT_UART", True)
|
||||||
|
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
|
||||||
|
zephyr_add_prj_conf("BASE64", True)
|
||||||
|
zephyr_add_prj_conf("CONSOLE", True)
|
||||||
|
# needed ?
|
||||||
|
zephyr_add_prj_conf("SERIAL", True)
|
||||||
|
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||||
|
zephyr_add_overlay(
|
||||||
|
"""
|
||||||
|
/ {
|
||||||
|
chosen {
|
||||||
|
zephyr,uart-mcumgr = &cdc_acm_uart0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
&zephyr_udc0 {
|
||||||
|
cdc_acm_uart0: cdc_acm_uart0 {
|
||||||
|
compatible = "zephyr,cdc-acm-uart";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
await cg.register_component(var, config)
|
||||||
80
esphome/components/fota/bluetooth.c
Normal file
80
esphome/components/fota/bluetooth.c
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2012-2014 Wind River Systems, Inc.
|
||||||
|
* Copyright (c) 2020 Prevas A/S
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <zephyr/bluetooth/bluetooth.h>
|
||||||
|
#include <zephyr/bluetooth/conn.h>
|
||||||
|
#include <zephyr/bluetooth/gatt.h>
|
||||||
|
#include <zephyr/mgmt/mcumgr/transport/smp_bt.h>
|
||||||
|
|
||||||
|
#define LOG_LEVEL LOG_LEVEL_DBG
|
||||||
|
#include <zephyr/logging/log.h>
|
||||||
|
LOG_MODULE_REGISTER(smp_bt_sample);
|
||||||
|
|
||||||
|
static struct k_work advertise_work;
|
||||||
|
|
||||||
|
static const struct bt_data ad[] = {
|
||||||
|
BT_DATA_BYTES(BT_DATA_FLAGS, (BT_LE_AD_GENERAL | BT_LE_AD_NO_BREDR)),
|
||||||
|
BT_DATA_BYTES(BT_DATA_UUID128_ALL,
|
||||||
|
0x84, 0xaa, 0x60, 0x74, 0x52, 0x8a, 0x8b, 0x86,
|
||||||
|
0xd3, 0x4c, 0xb7, 0x1d, 0x1d, 0xdc, 0x53, 0x8d),
|
||||||
|
};
|
||||||
|
|
||||||
|
static void advertise(struct k_work *work)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
bt_le_adv_stop();
|
||||||
|
|
||||||
|
rc = bt_le_adv_start(BT_LE_ADV_CONN_NAME, ad, ARRAY_SIZE(ad), NULL, 0);
|
||||||
|
if (rc) {
|
||||||
|
LOG_ERR("Advertising failed to start (rc %d)", rc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_INF("Advertising successfully started");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void connected(struct bt_conn *conn, uint8_t err)
|
||||||
|
{
|
||||||
|
if (err) {
|
||||||
|
LOG_ERR("Connection failed (err 0x%02x)", err);
|
||||||
|
} else {
|
||||||
|
LOG_INF("Connected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void disconnected(struct bt_conn *conn, uint8_t reason)
|
||||||
|
{
|
||||||
|
LOG_INF("Disconnected (reason 0x%02x)", reason);
|
||||||
|
k_work_submit(&advertise_work);
|
||||||
|
}
|
||||||
|
|
||||||
|
BT_CONN_CB_DEFINE(conn_callbacks) = {
|
||||||
|
.connected = connected,
|
||||||
|
.disconnected = disconnected,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void bt_ready(int err)
|
||||||
|
{
|
||||||
|
if (err != 0) {
|
||||||
|
LOG_ERR("Bluetooth failed to initialise: %d", err);
|
||||||
|
} else {
|
||||||
|
k_work_submit(&advertise_work);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_smp_bluetooth_adverts(void)
|
||||||
|
{
|
||||||
|
int rc;
|
||||||
|
|
||||||
|
k_work_init(&advertise_work, advertise);
|
||||||
|
rc = bt_enable(bt_ready);
|
||||||
|
|
||||||
|
if (rc != 0) {
|
||||||
|
LOG_ERR("Bluetooth enable failed: %d", rc);
|
||||||
|
}
|
||||||
|
}
|
||||||
18
esphome/components/fota/fota_component.cpp
Normal file
18
esphome/components/fota/fota_component.cpp
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#include "fota_component.h"
|
||||||
|
#include <zephyr/usb/usb_device.h>
|
||||||
|
#include <zephyr/mgmt/mcumgr/mgmt/callbacks.h>
|
||||||
|
|
||||||
|
extern "C" void start_smp_bluetooth_adverts();
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace fota {
|
||||||
|
|
||||||
|
void FOTAComponent::setup() {
|
||||||
|
if (IS_ENABLED(CONFIG_USB_DEVICE_STACK)) {
|
||||||
|
usb_enable(NULL);
|
||||||
|
}
|
||||||
|
start_smp_bluetooth_adverts();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fota
|
||||||
|
} // namespace esphome
|
||||||
14
esphome/components/fota/fota_component.h
Normal file
14
esphome/components/fota/fota_component.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
|
||||||
|
namespace esphome {
|
||||||
|
namespace fota {
|
||||||
|
|
||||||
|
class FOTAComponent : public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fota
|
||||||
|
} // namespace esphome
|
||||||
@@ -40,7 +40,7 @@ from esphome.components.libretiny.const import (
|
|||||||
COMPONENT_BK72XX,
|
COMPONENT_BK72XX,
|
||||||
COMPONENT_RTL87XX,
|
COMPONENT_RTL87XX,
|
||||||
)
|
)
|
||||||
from esphome.components.nrf52 import add_zephyr_overlay, add_zephyr_prj_conf_option
|
from esphome.components.nrf52.zephyr import zephyr_add_overlay, zephyr_add_prj_conf
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
logger_ns = cg.esphome_ns.namespace("logger")
|
logger_ns = cg.esphome_ns.namespace("logger")
|
||||||
@@ -298,9 +298,20 @@ async def to_code(config):
|
|||||||
|
|
||||||
if CORE.using_zephyr:
|
if CORE.using_zephyr:
|
||||||
if config[CONF_HARDWARE_UART] == UART0:
|
if config[CONF_HARDWARE_UART] == UART0:
|
||||||
add_zephyr_overlay("""&uart0 { status = "okay";};""")
|
zephyr_add_overlay("""&uart0 { status = "okay";};""")
|
||||||
if config[CONF_HARDWARE_UART] == USB_CDC:
|
if config[CONF_HARDWARE_UART] == USB_CDC:
|
||||||
add_zephyr_prj_conf_option("CONFIG_UART_LINE_CTRL", True)
|
zephyr_add_prj_conf("UART_LINE_CTRL", True)
|
||||||
|
zephyr_add_prj_conf("USB_DEVICE_STACK", True)
|
||||||
|
zephyr_add_prj_conf("USB_CDC_ACM", True)
|
||||||
|
zephyr_add_overlay(
|
||||||
|
"""
|
||||||
|
&zephyr_udc0 {
|
||||||
|
cdc_acm_uart0: cdc_acm_uart0 {
|
||||||
|
compatible = "zephyr,cdc-acm-uart";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Register at end for safe mode
|
# Register at end for safe mode
|
||||||
await cg.register_component(log, config)
|
await cg.register_component(log, config)
|
||||||
|
|||||||
@@ -13,21 +13,25 @@ from esphome.const import (
|
|||||||
)
|
)
|
||||||
from esphome.core import CORE, coroutine_with_priority
|
from esphome.core import CORE, coroutine_with_priority
|
||||||
from esphome.helpers import (
|
from esphome.helpers import (
|
||||||
write_file_if_changed,
|
|
||||||
copy_file_if_changed,
|
copy_file_if_changed,
|
||||||
)
|
)
|
||||||
from typing import Union
|
|
||||||
|
from .zephyr import (
|
||||||
|
zephyr_copy_files,
|
||||||
|
zephyr_set_core_data,
|
||||||
|
zephyr_to_code,
|
||||||
|
)
|
||||||
|
from .const import (
|
||||||
|
ZEPHYR_VARIANT_GENERIC,
|
||||||
|
ZEPHYR_VARIANT_NRF_SDK,
|
||||||
|
)
|
||||||
|
|
||||||
# force import gpio to register pin schema
|
# force import gpio to register pin schema
|
||||||
from .gpio import nrf52_pin_to_code # noqa
|
from .gpio import nrf52_pin_to_code # noqa
|
||||||
|
|
||||||
KEY_NRF52 = "nrf52"
|
|
||||||
|
|
||||||
|
|
||||||
def set_core_data(config):
|
def set_core_data(config):
|
||||||
CORE.data[KEY_NRF52] = {}
|
zephyr_set_core_data(config)
|
||||||
CORE.data[KEY_NRF52][KEY_PRJ_CONF_OPTIONS] = {}
|
|
||||||
CORE.data[KEY_NRF52][KEY_ZEPHYR_OVERLAY] = ""
|
|
||||||
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
CORE.data[KEY_CORE][KEY_TARGET_PLATFORM] = PLATFORM_NRF52
|
||||||
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = config[CONF_FRAMEWORK][CONF_TYPE]
|
CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = config[CONF_FRAMEWORK][CONF_TYPE]
|
||||||
return config
|
return config
|
||||||
@@ -66,8 +70,6 @@ PLATFORM_FRAMEWORK_SCHEMA = cv.All(
|
|||||||
_platform_check_versions,
|
_platform_check_versions,
|
||||||
)
|
)
|
||||||
|
|
||||||
ZEPHYR_VARIANT_GENERIC = "generic"
|
|
||||||
ZEPHYR_VARIANT_NRF_SDK = "nrf-sdk"
|
|
||||||
ZEPHYR_VARIANTS = [
|
ZEPHYR_VARIANTS = [
|
||||||
ZEPHYR_VARIANT_GENERIC,
|
ZEPHYR_VARIANT_GENERIC,
|
||||||
ZEPHYR_VARIANT_NRF_SDK,
|
ZEPHYR_VARIANT_NRF_SDK,
|
||||||
@@ -102,30 +104,6 @@ CONFIG_SCHEMA = cv.All(
|
|||||||
nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
nrf52_ns = cg.esphome_ns.namespace("nrf52")
|
||||||
|
|
||||||
|
|
||||||
PrjConfValueType = Union[bool, str, int]
|
|
||||||
KEY_PRJ_CONF_OPTIONS = "prj_conf_options"
|
|
||||||
KEY_ZEPHYR_OVERLAY = "zephyr_overlay"
|
|
||||||
|
|
||||||
|
|
||||||
def add_zephyr_prj_conf_option(name: str, value: PrjConfValueType):
|
|
||||||
"""Set an zephyr prj conf value."""
|
|
||||||
if not CORE.using_zephyr:
|
|
||||||
raise ValueError("Not an zephyr project")
|
|
||||||
if name in CORE.data[KEY_NRF52][KEY_PRJ_CONF_OPTIONS]:
|
|
||||||
old_value = CORE.data[KEY_NRF52][KEY_PRJ_CONF_OPTIONS][name]
|
|
||||||
if old_value != value:
|
|
||||||
raise ValueError(
|
|
||||||
f"{name} alread set with value {old_value}, new value {value}"
|
|
||||||
)
|
|
||||||
CORE.data[KEY_NRF52][KEY_PRJ_CONF_OPTIONS][name] = value
|
|
||||||
|
|
||||||
|
|
||||||
def add_zephyr_overlay(content):
|
|
||||||
if not CORE.using_zephyr:
|
|
||||||
raise ValueError("Not an zephyr project")
|
|
||||||
CORE.data[KEY_NRF52][KEY_ZEPHYR_OVERLAY] += content
|
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(1000)
|
@coroutine_with_priority(1000)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add(nrf52_ns.setup_preferences())
|
cg.add(nrf52_ns.setup_preferences())
|
||||||
@@ -152,6 +130,7 @@ async def to_code(config):
|
|||||||
cg.add_platformio_option("board_upload.use_1200bps_touch", "true")
|
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.require_upload_port", "true")
|
||||||
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
|
cg.add_platformio_option("board_upload.wait_for_upload_port", "true")
|
||||||
|
#
|
||||||
cg.add_platformio_option("extra_scripts", [f"pre:build_{conf[CONF_TYPE]}.py"])
|
cg.add_platformio_option("extra_scripts", [f"pre:build_{conf[CONF_TYPE]}.py"])
|
||||||
if CORE.using_arduino:
|
if CORE.using_arduino:
|
||||||
cg.add_build_flag("-DUSE_ARDUINO")
|
cg.add_build_flag("-DUSE_ARDUINO")
|
||||||
@@ -166,71 +145,21 @@ async def to_code(config):
|
|||||||
)
|
)
|
||||||
cg.add_library("https://github.com/NordicSemiconductor/nrfx#v2.1.0", None, None)
|
cg.add_library("https://github.com/NordicSemiconductor/nrfx#v2.1.0", None, None)
|
||||||
elif CORE.using_zephyr:
|
elif CORE.using_zephyr:
|
||||||
cg.add_build_flag("-DUSE_ZEPHYR")
|
zephyr_to_code(conf)
|
||||||
if conf[CONF_VARIANT] == ZEPHYR_VARIANT_GENERIC:
|
|
||||||
cg.add_platformio_option(
|
|
||||||
"platform_packages",
|
|
||||||
[
|
|
||||||
"platformio/framework-zephyr@^2.30500.231204",
|
|
||||||
# "platformio/toolchain-gccarmnoneeabi@^1.120301.0"
|
|
||||||
],
|
|
||||||
)
|
|
||||||
elif conf[CONF_VARIANT] == ZEPHYR_VARIANT_NRF_SDK:
|
|
||||||
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",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError
|
|
||||||
# c++ support
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_NEWLIB_LIBC", False)
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_NEWLIB_LIBC_NANO", True)
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_NEWLIB_LIBC_FLOAT_PRINTF", True)
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_CPLUSPLUS", True)
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_LIB_CPLUSPLUS", True)
|
|
||||||
# watchdog
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_WATCHDOG", True)
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_WDT_DISABLE_AT_BOOT", False)
|
|
||||||
# TODO debug only
|
|
||||||
add_zephyr_prj_conf_option("CONFIG_DEBUG_THREAD_INFO", True)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
# add_zephyr_prj_conf_option("CONFIG_USE_SEGGER_RTT", True)
|
# zephyr_add_prj_conf("USE_SEGGER_RTT", True)
|
||||||
# add_zephyr_prj_conf_option("CONFIG_RTT_CONSOLE", True)
|
# zephyr_add_prj_conf("RTT_CONSOLE", True)
|
||||||
# add_zephyr_prj_conf_option("CONFIG_UART_CONSOLE", False)
|
# zephyr_add_prj_conf("UART_CONSOLE", False)
|
||||||
|
|
||||||
|
# zephyr_add_prj_conf("LOG", True)
|
||||||
def _format_prj_conf_val(value: PrjConfValueType) -> str:
|
# zephyr_add_prj_conf("MCUBOOT_UTIL_LOG_LEVEL_WRN", True)
|
||||||
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
|
|
||||||
|
|
||||||
|
|
||||||
# Called by writer.py
|
# Called by writer.py
|
||||||
def copy_files():
|
def copy_files():
|
||||||
if CORE.using_zephyr:
|
if CORE.using_zephyr:
|
||||||
want_opts = CORE.data[KEY_NRF52][KEY_PRJ_CONF_OPTIONS]
|
zephyr_copy_files()
|
||||||
contents = (
|
|
||||||
"\n".join(
|
|
||||||
f"{name}={_format_prj_conf_val(value)}"
|
|
||||||
for name, value in sorted(want_opts.items())
|
|
||||||
)
|
|
||||||
+ "\n"
|
|
||||||
)
|
|
||||||
|
|
||||||
write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), contents)
|
|
||||||
write_file_if_changed(
|
|
||||||
CORE.relative_build_path("zephyr/app.overlay"),
|
|
||||||
CORE.data[KEY_NRF52][KEY_ZEPHYR_OVERLAY],
|
|
||||||
)
|
|
||||||
|
|
||||||
dir = os.path.dirname(__file__)
|
dir = os.path.dirname(__file__)
|
||||||
build_zephyr_file = os.path.join(
|
build_zephyr_file = os.path.join(
|
||||||
|
|||||||
5
esphome/components/nrf52/const.py
Normal file
5
esphome/components/nrf52/const.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
KEY_ZEPHYR = "zephyr"
|
||||||
|
KEY_PRJ_CONF = "prj_conf"
|
||||||
|
KEY_OVERLAY = "overlay"
|
||||||
|
ZEPHYR_VARIANT_GENERIC = "generic"
|
||||||
|
ZEPHYR_VARIANT_NRF_SDK = "nrf-sdk"
|
||||||
106
esphome/components/nrf52/zephyr.py
Normal file
106
esphome/components/nrf52/zephyr.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import esphome.codegen as cg
|
||||||
|
from typing import Union
|
||||||
|
from esphome.core import CORE
|
||||||
|
from esphome.helpers import (
|
||||||
|
write_file_if_changed,
|
||||||
|
)
|
||||||
|
from .const import (
|
||||||
|
ZEPHYR_VARIANT_GENERIC,
|
||||||
|
KEY_ZEPHYR,
|
||||||
|
KEY_PRJ_CONF,
|
||||||
|
KEY_OVERLAY,
|
||||||
|
ZEPHYR_VARIANT_NRF_SDK,
|
||||||
|
)
|
||||||
|
from esphome.const import (
|
||||||
|
CONF_VARIANT,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_set_core_data(config):
|
||||||
|
CORE.data[KEY_ZEPHYR] = {}
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF] = {}
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_OVERLAY] = ""
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
PrjConfValueType = Union[bool, str, int]
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_prj_conf(name: str, value: PrjConfValueType):
|
||||||
|
"""Set an zephyr prj conf value."""
|
||||||
|
if not CORE.using_zephyr:
|
||||||
|
raise ValueError("Not an zephyr project")
|
||||||
|
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 != value:
|
||||||
|
raise ValueError(
|
||||||
|
f"{name} alread set with value {old_value}, new value {value}"
|
||||||
|
)
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF][name] = value
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_add_overlay(content):
|
||||||
|
if not CORE.using_zephyr:
|
||||||
|
raise ValueError("Not an zephyr project")
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_OVERLAY] += content
|
||||||
|
|
||||||
|
|
||||||
|
def zephyr_to_code(conf):
|
||||||
|
cg.add_build_flag("-DUSE_ZEPHYR")
|
||||||
|
if conf[CONF_VARIANT] == ZEPHYR_VARIANT_GENERIC:
|
||||||
|
cg.add_platformio_option(
|
||||||
|
"platform_packages",
|
||||||
|
[
|
||||||
|
"platformio/framework-zephyr@^2.30500.231204",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
elif conf[CONF_VARIANT] == ZEPHYR_VARIANT_NRF_SDK:
|
||||||
|
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",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError
|
||||||
|
# c++ support
|
||||||
|
zephyr_add_prj_conf("NEWLIB_LIBC", False)
|
||||||
|
zephyr_add_prj_conf("NEWLIB_LIBC_NANO", True)
|
||||||
|
zephyr_add_prj_conf("NEWLIB_LIBC_FLOAT_PRINTF", True)
|
||||||
|
zephyr_add_prj_conf("CPLUSPLUS", True)
|
||||||
|
zephyr_add_prj_conf("LIB_CPLUSPLUS", True)
|
||||||
|
# watchdog
|
||||||
|
zephyr_add_prj_conf("WATCHDOG", True)
|
||||||
|
zephyr_add_prj_conf("WDT_DISABLE_AT_BOOT", False)
|
||||||
|
# TODO debug only
|
||||||
|
# zephyr_add_prj_conf("DEBUG_THREAD_INFO", True)
|
||||||
|
|
||||||
|
|
||||||
|
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_copy_files():
|
||||||
|
want_opts = CORE.data[KEY_ZEPHYR][KEY_PRJ_CONF]
|
||||||
|
contents = (
|
||||||
|
"\n".join(
|
||||||
|
f"{name}={_format_prj_conf_val(value)}"
|
||||||
|
for name, value in sorted(want_opts.items())
|
||||||
|
)
|
||||||
|
+ "\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
write_file_if_changed(CORE.relative_build_path("zephyr/prj.conf"), contents)
|
||||||
|
write_file_if_changed(
|
||||||
|
CORE.relative_build_path("zephyr/app.overlay"),
|
||||||
|
CORE.data[KEY_ZEPHYR][KEY_OVERLAY],
|
||||||
|
)
|
||||||
@@ -555,6 +555,7 @@ CONF_OSCILLATION_COMMAND_TOPIC = "oscillation_command_topic"
|
|||||||
CONF_OSCILLATION_OUTPUT = "oscillation_output"
|
CONF_OSCILLATION_OUTPUT = "oscillation_output"
|
||||||
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
|
CONF_OSCILLATION_STATE_TOPIC = "oscillation_state_topic"
|
||||||
CONF_OTA = "ota"
|
CONF_OTA = "ota"
|
||||||
|
CONF_FOTA = "fota"
|
||||||
CONF_OUTPUT = "output"
|
CONF_OUTPUT = "output"
|
||||||
CONF_OUTPUT_ID = "output_id"
|
CONF_OUTPUT_ID = "output_id"
|
||||||
CONF_OUTPUTS = "outputs"
|
CONF_OUTPUTS = "outputs"
|
||||||
|
|||||||
130
esphome/zephyr_tools.py
Normal file
130
esphome/zephyr_tools.py
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
from typing import Final
|
||||||
|
from rich.pretty import pprint
|
||||||
|
from bleak import BleakScanner
|
||||||
|
from bleak.exc import BleakDeviceNotFoundError
|
||||||
|
from smpclient.transport.ble import SMPBLETransport
|
||||||
|
from smpclient.transport.serial import SMPSerialTransport
|
||||||
|
from smpclient import SMPClient
|
||||||
|
from smpclient.mcuboot import IMAGE_TLV, ImageInfo, TLVNotFound
|
||||||
|
from smpclient.requests.image_management import ImageStatesRead, ImageStatesWrite
|
||||||
|
from smpclient.requests.os_management import ResetWrite
|
||||||
|
from pyocd.tools.lists import ListGenerator
|
||||||
|
|
||||||
|
from smpclient.generics import error, success
|
||||||
|
from esphome.espota2 import ProgressBar
|
||||||
|
|
||||||
|
SMP_SERVICE_UUID = "8D53DC1D-1DB7-4CD3-868B-8A527460AA84"
|
||||||
|
MAC_ADDRESS_PATTERN: Final = re.compile(
|
||||||
|
r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE
|
||||||
|
)
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def smpmgr_scan():
|
||||||
|
_LOGGER.info("Scanning bluetooth...")
|
||||||
|
devices = await BleakScanner.discover(service_uuids=[SMP_SERVICE_UUID])
|
||||||
|
return devices
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_tlv_sha256(file):
|
||||||
|
_LOGGER.info(f"Checking image: {str(file)}")
|
||||||
|
try:
|
||||||
|
image_info = ImageInfo.load_file(str(file))
|
||||||
|
pprint(image_info.header)
|
||||||
|
_LOGGER.debug(str(image_info))
|
||||||
|
except Exception as e:
|
||||||
|
_LOGGER.error(f"Inspection of FW image failed: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
image_tlv_sha256 = image_info.get_tlv(IMAGE_TLV.SHA256)
|
||||||
|
_LOGGER.debug(f"IMAGE_TLV_SHA256: {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):
|
||||||
|
image_tlv_sha256 = get_image_tlv_sha256(firmware)
|
||||||
|
if image_tlv_sha256 is None:
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if MAC_ADDRESS_PATTERN.match(host):
|
||||||
|
smp_client = SMPClient(SMPBLETransport(), host)
|
||||||
|
else:
|
||||||
|
smp_client = SMPClient(SMPSerialTransport(mtu=256), host)
|
||||||
|
|
||||||
|
_LOGGER.info(f"Connecting {host}...")
|
||||||
|
try:
|
||||||
|
await smp_client.connect()
|
||||||
|
except BleakDeviceNotFoundError:
|
||||||
|
_LOGGER.error(f"Device {host} not found")
|
||||||
|
return 1
|
||||||
|
|
||||||
|
_LOGGER.info(f"Connected {host}...")
|
||||||
|
|
||||||
|
image_state = await asyncio.wait_for(
|
||||||
|
smp_client.request(ImageStatesRead()), timeout=SMPClient.MEDIUM_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
already_uploaded = False
|
||||||
|
|
||||||
|
if error(image_state):
|
||||||
|
_LOGGER.error(image_state)
|
||||||
|
return 1
|
||||||
|
elif 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.hash == image_tlv_sha256:
|
||||||
|
if already_uploaded:
|
||||||
|
_LOGGER.error("Both slots have the same image")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
async for offset in smp_client.upload(image):
|
||||||
|
progress.update(offset / upload_size)
|
||||||
|
progress.done()
|
||||||
|
|
||||||
|
_LOGGER.info("Mark image for testing")
|
||||||
|
r = await asyncio.wait_for(
|
||||||
|
smp_client.request(ImageStatesWrite(hash=image_tlv_sha256)),
|
||||||
|
timeout=SMPClient.SHORT_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
if error(r):
|
||||||
|
_LOGGER.error(r)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
_LOGGER.info("Reset")
|
||||||
|
r = await asyncio.wait_for(
|
||||||
|
smp_client.request(ResetWrite()), timeout=SMPClient.SHORT_TIMEOUT
|
||||||
|
)
|
||||||
|
|
||||||
|
if error(r):
|
||||||
|
_LOGGER.error(r)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def list_pyocd():
|
||||||
|
return ListGenerator.list_probes()["boards"]
|
||||||
@@ -25,3 +25,15 @@ pyparsing >= 3.0
|
|||||||
|
|
||||||
# For autocompletion
|
# For autocompletion
|
||||||
argcomplete>=2.0.0
|
argcomplete>=2.0.0
|
||||||
|
|
||||||
|
# for mcumgr
|
||||||
|
git+https://github.com/tomaszduda23/smp/#f9b85266485062f6a466989e8f59b913cc83b08b
|
||||||
|
git+https://github.com/tomaszduda23/smpclient/#d25c8035ae2858fd41a106058297b619d58fbcb5
|
||||||
|
# move to framework?
|
||||||
|
git+https://github.com/tomaszduda23/pyOCD/#949193f7cbf09081f8e46d6b9d2e4a79e536997e
|
||||||
|
bleak==0.21.1
|
||||||
|
pydantic==2.16.2
|
||||||
|
cbor2==5.6.1
|
||||||
|
crcmod==1.7
|
||||||
|
# pretty print by logging?
|
||||||
|
rich==13.7.0
|
||||||
|
|||||||
Reference in New Issue
Block a user