esphome icon indicating copy to clipboard operation
esphome copied to clipboard

[nrf52] Add openthread support

Open felipejfc opened this issue 8 months ago • 17 comments

What does this implement/fix?

Supporting openthread in nrf52 devices, enabling ipv6 connectivity through openthread border router which unlocks ip based functionality such as mdns discoverability, api component, mqtt etc.

It requires:

  • https://github.com/esphome/esphome/pull/7049
  • https://github.com/esphome/esphome/pull/6075

TODO

  • [x] Openthread basic support for nrf52
  • [x] Internet connectivity with ipv6
  • [x] Support esphome api component
  • [x] OTA with mcumgr udp
  • [x] Logs in dashboard over thread
  • [x] OTA glue into esphome dashboard
  • [x] Factory reset pin
  • [x] Move MCUMGR transport restart logic to mcumgr module
  • [ ] Board configurations to allow e.g. external flash
  • [ ] Add RCP support
  • [x] Support api encryption
  • [ ] Allow moving settings to external storage
  • [ ] Allow specifying custom pm_static
  • [ ] Cleanup code
  • [ ] Tests

Types of changes

  • [ ] Bugfix (non-breaking change which fixes an issue)
  • [x] New feature (non-breaking change which adds functionality)
  • [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • [ ] Code quality improvements to existing code or addition of tests
  • [ ] Other

Related issue or feature (if applicable):

  • https://github.com/esphome/esphome/pull/7506

Test Environment

  • [ ] ESP32
  • [ ] ESP32 IDF
  • [ ] ESP8266
  • [ ] RP2040
  • [ ] BK72xx
  • [ ] RTL87xx
  • [x] nrf52840

Example entry for config.yaml:

# Example config.yaml

external_components:
  - source: github://pr#8410
    components: [ logger, nrf52, time, zephyr, zephyr_openthread, mdns, network ]
    refresh: always

nrf52:
  board: adafruit_itsybitsy_nrf52840

esphome:
  name: nrf52-ot
logger:
  level: DEBUG
  logs:
    switch: NONE

switch:
  - platform: gpio
    name: "Led Light"
    pin:
      number: 15
      inverted: true
    id: gpio_15

output:
  - platform: gpio
    pin:
      number: 14
      inverted: true
      mode:
        output: true
    id: rest_gpio

# Enable IPv6 networking
network:
  enable_ipv6: True

# Enable OpenThread
zephyr_openthread:
  channel: 15
  panid: 0x1234
  network_name: "ha-thread-228c"
  xpanid: "1111111122222222"
  network_key: "00112233445566778899aabbccddeeff"
  pskc: "00112233445566778899aabbccddeeff"
  radio_tx_power: 0
  force_dataset: true

Checklist:

  • [x] The code change is tested and works locally.
  • [ ] Tests have been added to verify that the new code works (under tests/ folder).

If user exposed functionality or configuration variables are added/changed:

felipejfc avatar Mar 15 '25 12:03 felipejfc

Codecov Report

Attention: Patch coverage is 27.27273% with 8 lines in your changes missing coverage. Please review.

Project coverage is 54.90%. Comparing base (4d8b5ed) to head (1c8d5b6). Report is 2285 commits behind head on dev.

Files with missing lines Patch % Lines
esphome/config_validation.py 0.00% 7 Missing :warning:
esphome/core/__init__.py 66.66% 1 Missing :warning:
Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #8410      +/-   ##
==========================================
+ Coverage   53.70%   54.90%   +1.19%     
==========================================
  Files          50       50              
  Lines        9408     9925     +517     
  Branches     1654     1357     -297     
==========================================
+ Hits         5053     5449     +396     
- Misses       4056     4133      +77     
- Partials      299      343      +44     

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

:rocket: New features to boost your workflow:
  • :snowflake: Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

codecov-commenter avatar Mar 15 '25 12:03 codecov-commenter

It works :+1:

external_components:
  - source: github://pr#7049
    components: [ logger, nrf52, zephyr, time ]
    refresh: always
  - source: github://pr#8410
    components: [ socket, zephyr_openthread, mdns, network ]
    refresh: always

nrf52:
  board: adafruit_itsybitsy_nrf52840
  bootloader: adafruit_nrf52_sd140_v6 # for nice nano

esphome:
  name: nrf52-test-nrf

logger:
  level: DEBUG

switch:

  - platform: gpio
    pin:
      number: 15
      mode:
        output: true
    id: gpio_15

interval:
  - interval: 500ms
    then:
      - switch.toggle: gpio_15

socket:

network:
  enable_ipv6: True

# Enable OpenThread
zephyr_openthread:
  channel: 15
  panid: 0x1234
  network_name: "OpenThread-ESP"
  xpanid: ""
  network_key: ""
  pskc: ""
  radio_tx_power: 0
  force_dataset: true

tomaszduda23 avatar Mar 15 '25 15:03 tomaszduda23

once #8412 is merged and development gets easier by being able to build in the dev web interface, I will continue development

felipejfc avatar Mar 30 '25 22:03 felipejfc

once #8412 is merged and development gets easier by being able to build in the dev web interface, I will continue development

You could hardcode those temporarily. I did similar thing before other changes were merged.

tomaszduda23 avatar Apr 01 '25 14:04 tomaszduda23

did it! now possible to build from the dev device builder. next I will take a look into OTA so that its easier to iterate and afterwards, fixing API encryption

e.g.

external_components:
  - source: github://pr#8410
    components: [ logger, nrf52, time, zephyr, zephyr_openthread, mdns, network, socket, api ]
    refresh: always

nrf52:
  board: xiao_ble
  bootloader: mcuboot

esphome:
  name: nrf52-ot

api:

logger:
  level: DEBUG
  logs:
    switch: NONE

switch:
  - platform: gpio
    name: "Led Light"
    icon: "mdi:restart"
    pin:
      number: P0.06
      inverted: true
    id: gpio_15

# Enable IPv6 networking
network:
  enable_ipv6: true

# Enable OpenThread
zephyr_openthread:
  channel: 25
  panid: 0xe064
  network_name: "MyHome5"
  xpanid: "1db74a0a3797454b"
  network_key: "00112233445566778899AABBCCDDEEFF"
  pskc: "00112233445566778899AABBCCDDEEFF"
  force_dataset: true
  shell: false

# Add a text sensor to display the IPv6 address
text_sensor:
  - platform: template
    name: "Thread IPv6 Address"
    id: thread_ipv6
    update_interval: 60s
    lambda: |-
      if (zephyr_openthread::global_openthread_component->has_ipv6_address()) {
        return zephyr_openthread::global_openthread_component->get_ipv6_address();
      }
      return {"No IPv6 Address"};

felipejfc avatar Apr 05 '25 16:04 felipejfc

This is independent of PR https://github.com/esphome/esphome/pull/7506 ?

rwrozelle avatar Apr 08 '25 15:04 rwrozelle

This is independent of PR #7506 ?

As stated in the first comment, PR #7506 is related, but not a dependency.

Related issue or feature (if applicable):

* [Add OpenThread support on ESP-IDF #7506](https://github.com/esphome/esphome/pull/7506)

potelux avatar Apr 08 '25 20:04 potelux

made good progress with ota. managed to get it working with mcumgr using udp transport over thread. rebased the work in #6075 ota works already by calling mcumgr manually but will work on the glue with esphome dashboard

felipejfc avatar Apr 11 '25 03:04 felipejfc

ota works (needs to run esphome dashboard from this pr/branch) e.g.

external_components:
  - source: github://pr#8410
    components: [ logger, nrf52, time, zephyr, zephyr_openthread, mdns, network, socket, api, openthread_info, ota, zephyr_mcumgr ]
    refresh: always

nrf52:
  board: xiao_ble
  bootloader: mcuboot

esphome:
  name: nrf52-ot

api:

logger:
  level: DEBUG
  logs:
    switch: NONE
  hardware_uart: uart0

switch:
  - platform: gpio
    name: "Led Light"
    icon: "mdi:restart"
    pin:
      number: P0.06
      inverted: true
    id: gpio_15

# Enable IPv6 networking
network:
  enable_ipv6: true

# Enable OpenThread
zephyr_openthread:
  channel: 25
  panid: 0xf060
  network_name: "Network"
  xpanid: "1122334455667788"
  network_key: "112233445566778899aabbccddeeff00"
  pskc: "112233445566778899aabbccddeeff00"
  force_dataset: true
  shell: false

ota:
  - platform: zephyr_mcumgr
    transport: udp
    on_begin:
      then:
        - logger.log: "OTA start"
    on_progress:
      then:
        - logger.log:
            format: "OTA progress %0.1f%%"
            args: ["x"]
    on_end:
      then:
        - logger.log: "OTA end"
    on_error:
      then:
        - logger.log:
            format: "OTA update error %d"
            args: ["x"]
    on_state_change:
      then:
        lambda: >-
          ESP_LOGD("ota", "State %d", state);

# Then add the text sensor for IPv6 addresses
text_sensor:
  - platform: openthread_info
    ip_address:
      name: "Thread IPv6 Addresses"
      update_interval: 30s
    role:
      name: "Thread Role"
      update_interval: 30s
    channel:
      name: "Thread Channel"
      update_interval: 30s
    network_name:
      name: "Thread Network Name"
      update_interval: 30s
    panid:
      name: "Thread PanID"
      update_interval: 30s

felipejfc avatar Apr 13 '25 03:04 felipejfc

  • [ ] Allow specifying custom pm_static It seems to be already implemented https://github.com/tomaszduda23/esphome/blob/fd8d7956af3b5d1923f37b85716a9ed83a96009e/esphome/components/nrf52/init.py#L50

tomaszduda23 avatar Apr 14 '25 14:04 tomaszduda23

  • [ ] Allow specifying custom pm_static It seems to be already implemented https://github.com/tomaszduda23/esphome/blob/fd8d7956af3b5d1923f37b85716a9ed83a96009e/esphome/components/nrf52/init.py#L50

I mean receive it from the esphome's device config yaml so the user can resize partitions if they want

felipejfc avatar Apr 14 '25 14:04 felipejfc

@tomaszduda23 maybe you can help me with something, when I try to compile with api encryption, platformio includes noise-c as a dependency, here: https://github.com/esphome/esphome/blob/dev/esphome/components/api/init.py#L154-L160 And then when I'm compiling, the linker is not able to find methods that should be provided by the lib, which is weird cause the generated platformio.conf file did insert the lib as a dependency:

; ========== AUTO GENERATED CODE BEGIN ===========
[common]
lib_deps =
build_flags =
upload_flags =

; ========== AUTO GENERATED CODE BEGIN ===========
[platformio]
description = ESPHome 2025.4.0-dev
[env:nrf52-ot]
board = xiao_ble
boards_dir = /home/felipe/dev/esphome/config/.esphome/build/nrf52-ot/boards
build_flags =
    -DCONFIG_LWIP_IPV6
    -DCONFIG_LWIP_IPV6_AUTOCONFIG
    -DESPHOME_LOG_LEVEL=ESPHOME_LOG_LEVEL_DEBUG
    -DUSE_IPV6
    -DUSE_NRF52
    -DUSE_OPENTHREAD
    -DUSE_ZEPHYR
    -DUSE_ZEPHYR_NETWORKING
    -DUSE_ZEPHYR_OPENTHREAD
    -Wno-sign-compare
    -Wno-unused-but-set-variable
    -Wno-unused-variable
    -fno-exceptions
extra_scripts =
    pre:/home/felipe/dev/esphome/esphome/components/zephyr/pre_build.py.script
framework = zephyr
lib_deps =
    esphome/[email protected]
    ${common.lib_deps}
platform = https://github.com/tomaszduda23/platform-nordicnrf52/archive/refs/tags/v10.3.0-1.zip
platform_packages =
    platformio/framework-zephyr@https://github.com/tomaszduda23/framework-sdk-nrf/archive/refs/tags/v2.6.1-4.zip
    platformio/toolchain-gccarmnoneeabi@https://github.com/tomaszduda23/toolchain-sdk-ng/archive/refs/tags/v0.16.1-1.zip
; =========== AUTO GENERATED CODE END ============

shouldn't the link happen automatically?

felipejfc avatar Apr 14 '25 16:04 felipejfc

Zephyr uses west build system. Esphome uses platformio. I take all complication flags, source files etc from platformio and generate cmake file. Than use west to build it. After that return result to platformio. Probably I missed something for lib_deps.

tomaszduda23 avatar Apr 15 '25 06:04 tomaszduda23

It should be handled somewhere in https://github.com/tomaszduda23/framework-sdk-nrf/blob/0c8827425a7955fbcfff4796c45d82cb396a2d55/scripts/platformio/platformio-build.py#L170

tomaszduda23 avatar Apr 15 '25 17:04 tomaszduda23

You can try https://github.com/tomaszduda23/framework-sdk-nrf/releases/tag/v2.6.1-6. It fix the linking issue. All source file are in single target. It mess up with flags a little bit.

tomaszduda23 avatar Apr 18 '25 17:04 tomaszduda23

Thx for that! I'm on vacations now. Will test as soon as I come back

=== EDIT

Tested and link now works! thx @tomaszduda23

felipejfc avatar Apr 18 '25 18:04 felipejfc

It seems there is a conflict to resolve here.

lboue avatar Jun 11 '25 15:06 lboue

For reference, ESPHome 2025.6 merged PR by @mrene with OpenThread component supporting ESP-IDF:

  • https://github.com/esphome/esphome/pull/7506

    • https://esphome.io/changelog/2025.6.0.html#openthread

      • https://esphome.io/components/openthread

PS: Off-topic but I think that with the introduction of native OpenThread in ESPHome there will a huge new interest in making ultra low-power battery-operated devices combining the Thread protocol and ESPHome, so wondering if you and/or others also have a roadmap for extending ESPHome built-in power-management funtionality for having ability to make sleepy end devices using generic sleeping technics (low-power sleep functionality) and/or even deep sleep features (practical power-off with wake-up timer) on all platforms supported by ESPHome?

Hedda avatar Jul 02 '25 16:07 Hedda

@felipejfc core part was just merged. I will rebase other PRs soon. That way most of the code in this PR won't be needed anymore.

tomaszduda23 avatar Jul 16 '25 04:07 tomaszduda23

Any updates on this?

Hedda avatar Aug 20 '25 11:08 Hedda

Could you indicate which files are in conflict? There is an error building this:

INFO ESPHome 2025.11.0-dev
INFO Reading configuration /config/esphome/xiao-nrf52-ot.yaml...
INFO Updating https://github.com/esphome/esphome.git@pull/8410/head
ERROR Unable to import component zephyr_openthread:
Traceback (most recent call last):
  File "/esphome/esphome/loader.py", line 199, in _lookup_module
    module = importlib.import_module(f"esphome.components.{domain}")
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/importlib/__init__.py", line 90, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 999, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/data/external_components/aa491595/esphome/components/zephyr_openthread/__init__.py", line 10, in <module>
    from esphome.const import CONF_ID, CONF_ENABLE_IPV6, CONF_OTA, CONF_TRANSPORT, CONF_UDP
ImportError: cannot import name 'CONF_TRANSPORT' from 'esphome.const' (/esphome/esphome/const.py)
Failed config

zephyr_openthread: [source /config/esphome/xiao-nrf52-ot.yaml:51]

  Component not found: zephyr_openthread.
  channel: 15
  panid: 4660
  network_name: ha-thread-228c
  xpanid: 1111111122222222
  network_key: 00112233445566778899aabbccddeeff
  pskc: 00112233445566778899aabbccddeeff
  radio_tx_power: 0
  force_dataset: True

lboue avatar Nov 02 '25 07:11 lboue

This PR has a lot of old stuff which is not related to OT. Probably it would be easier to open new one and take OT/network related things only.

tomaszduda23 avatar Nov 02 '25 09:11 tomaszduda23

zephyr_openthread

Do we have to use the existing openthread componant or keep the zephyr_openthread componant?

lboue avatar Nov 02 '25 12:11 lboue

Having the same config for all is better.

tomaszduda23 avatar Nov 02 '25 23:11 tomaszduda23