diff --git a/openthread_border_router/CHANGELOG.md b/openthread_border_router/CHANGELOG.md index 1ad7d66e06a..53607d46e29 100644 --- a/openthread_border_router/CHANGELOG.md +++ b/openthread_border_router/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2.16.1 +- Preserve Thread network identity when switching between stable and beta mode + ## 2.16.0 - Add beta toggle to switch between Thread 1.3 (stable) and Thread 1.4 (beta) - Beta mode uses OpenThread's built-in mDNS instead of mDNSResponder diff --git a/openthread_border_router/config.yaml b/openthread_border_router/config.yaml index 7febfbe7f9b..5a10e463032 100644 --- a/openthread_border_router/config.yaml +++ b/openthread_border_router/config.yaml @@ -1,5 +1,5 @@ --- -version: 2.16.0 +version: 2.16.1 slug: openthread_border_router name: OpenThread Border Router description: OpenThread Border Router add-on diff --git a/openthread_border_router/rootfs/etc/s6-overlay/s6-rc.d/otbr-agent/run b/openthread_border_router/rootfs/etc/s6-overlay/s6-rc.d/otbr-agent/run index 94dcbcccc44..ec0b7969a5b 100755 --- a/openthread_border_router/rootfs/etc/s6-overlay/s6-rc.d/otbr-agent/run +++ b/openthread_border_router/rootfs/etc/s6-overlay/s6-rc.d/otbr-agent/run @@ -148,10 +148,18 @@ echo "${otbr_rest_listen_port}" >> /tmp/otbr-agent-rest-api # Migrate OTBR settings to new adapter if needed bashio::log.info "Migrating OTBR settings if needed..." + +if bashio::config.true 'beta'; then + thread_version=5 +else + thread_version=4 +fi + python3 /usr/local/bin/migrate_otbr_settings.py \ --adapter "${device}" \ --baudrate "${baudrate}" \ --flow-control "${migrate_flow_control}" \ + --thread-version "${thread_version}" \ --data-dir /data/thread/ bashio::log.info "Starting otbr-agent..." diff --git a/openthread_border_router/rootfs/usr/local/bin/migrate_otbr_settings.py b/openthread_border_router/rootfs/usr/local/bin/migrate_otbr_settings.py index 0c1a6411f51..aecfe69e878 100755 --- a/openthread_border_router/rootfs/usr/local/bin/migrate_otbr_settings.py +++ b/openthread_border_router/rootfs/usr/local/bin/migrate_otbr_settings.py @@ -1,9 +1,10 @@ import asyncio import argparse +import dataclasses import datetime -import zigpy.serial +from typing import Self +import serialx from pathlib import Path -from serialx import PinState from enum import Enum from universal_silabs_flasher.spinel import ( @@ -75,23 +76,71 @@ def is_valid_otbr_settings_file(settings: list[tuple[OtbrSettingsKey, bytes]]) - return {OtbrSettingsKey.ACTIVE_DATASET} <= {key for key, _ in settings} +@dataclasses.dataclass +class NetworkInfo: + """OpenThread NetworkInfo settings structure.""" + + role: int + device_mode: int + rloc16: int + key_sequence: int + mle_frame_counter: int + mac_frame_counter: int + previous_partition_id: int + ext_address: bytes + ml_iid: bytes + version: int + + @classmethod + def from_bytes(cls, data: bytes) -> Self: + return cls( + role=data[0], + device_mode=data[1], + rloc16=int.from_bytes(data[2:4], "little"), + key_sequence=int.from_bytes(data[4:8], "little"), + mle_frame_counter=int.from_bytes(data[8:12], "little"), + mac_frame_counter=int.from_bytes(data[12:16], "little"), + previous_partition_id=int.from_bytes(data[16:20], "little"), + ext_address=data[20:28], + ml_iid=data[28:36], + version=int.from_bytes(data[36:38], "little"), + ) + + def to_bytes(self) -> bytes: + return ( + self.role.to_bytes(1, "little") + + self.device_mode.to_bytes(1, "little") + + self.rloc16.to_bytes(2, "little") + + self.key_sequence.to_bytes(4, "little") + + self.mle_frame_counter.to_bytes(4, "little") + + self.mac_frame_counter.to_bytes(4, "little") + + self.previous_partition_id.to_bytes(4, "little") + + self.ext_address + + self.ml_iid + + self.version.to_bytes(2, "little") + ) + + async def get_adapter_hardware_addr( port: str, baudrate: int = 460800, flow_control: str | None = None ) -> str: loop = asyncio.get_running_loop() async with asyncio.timeout(CONNECT_TIMEOUT): - _, protocol = await zigpy.serial.create_serial_connection( - loop=loop, - protocol_factory=SpinelProtocol, + _, protocol = await serialx.create_serial_connection( + loop, + SpinelProtocol, url=port, baudrate=baudrate, - flow_control=flow_control, + xonxoff=(flow_control == "software"), + rtscts=(flow_control == "hardware"), # OTBR uses `uart-init-deassert` when flow control is disabled rtsdtr_on_open=( - PinState.HIGH if flow_control == "hardware" else PinState.LOW + serialx.PinState.HIGH + if flow_control == "hardware" + else serialx.PinState.LOW ), - rtsdtr_on_close=PinState.LOW, + rtsdtr_on_close=serialx.PinState.LOW, ) await protocol.wait_until_connected() @@ -142,6 +191,12 @@ async def main() -> None: default="none", help="Flow control for the serial connection (hardware, software, or none)", ) + parser.add_argument( + "--thread-version", + type=int, + default=None, + help="Thread version to set in NETWORK_INFO (4=1.3, 5=1.4)", + ) args = parser.parse_args() @@ -187,28 +242,49 @@ async def main() -> None: if expected_settings_path.exists(): if most_recent_settings_path == expected_settings_path: + # Check if thread version needs updating + current_version = None + for key, value in most_recent_settings: + if key == OtbrSettingsKey.NETWORK_INFO: + current_version = NetworkInfo.from_bytes(value).version + break + + if args.thread_version is None or current_version == args.thread_version: + print( + f"Adapter settings file {expected_settings_path} is the most recently used, skipping" + ) + return + print( - f"Adapter settings file {expected_settings_path} is the most recently used, skipping" + f"Updating thread version from {current_version} to {args.thread_version}" ) - return - - # If the settings file is old, we should "delete" it - print( - f"Settings file for adapter {hwaddr} already exists at {expected_settings_path} but appears to be old, archiving" - ) - backup_file(expected_settings_path) + else: + # If the settings file is old, we should "delete" it + print( + f"Settings file for adapter {hwaddr} already exists at {expected_settings_path} but appears to be old, archiving" + ) + backup_file(expected_settings_path) # Write back a new settings file that keeps only a few keys - new_settings = [ - (key, value) - for key, value in most_recent_settings - if key - in ( + new_settings = [] + + for key, value in most_recent_settings: + if key == OtbrSettingsKey.NETWORK_INFO and args.thread_version is not None: + network_info = NetworkInfo.from_bytes(value) + assert network_info.to_bytes() == value + + # To support transparent upgrades, we modify the Thread version + network_info = dataclasses.replace( + network_info, version=args.thread_version + ) + new_settings.append((key, network_info.to_bytes())) + elif key in ( OtbrSettingsKey.ACTIVE_DATASET, OtbrSettingsKey.PENDING_DATASET, OtbrSettingsKey.CHILD_INFO, - ) - ] + OtbrSettingsKey.NETWORK_INFO, + ): + new_settings.append((key, value)) expected_settings_path.write_bytes(serialize_otbr_settings(new_settings)) print(f"Wrote new settings file to {expected_settings_path}")