Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions openthread_border_router/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion openthread_border_router/config.yaml
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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..."
Expand Down
122 changes: 99 additions & 23 deletions openthread_border_router/rootfs/usr/local/bin/migrate_otbr_settings.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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}")
Expand Down