From 25c9a4f638b5e0cdf66acd0aaca04fb51b46b92a Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sat, 27 Jan 2024 13:40:52 +1100 Subject: [PATCH 1/6] Find gpio chip by name Chip number for USB gpio chips will depend on what other system gpio chips already exist on the system. Add support to search by chip label to find chip number. --- universal_silabs_flasher/flasher.py | 4 ++- universal_silabs_flasher/gpio.py | 39 ++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/universal_silabs_flasher/flasher.py b/universal_silabs_flasher/flasher.py index 1af0b11..db617c1 100644 --- a/universal_silabs_flasher/flasher.py +++ b/universal_silabs_flasher/flasher.py @@ -22,7 +22,7 @@ from .emberznet import connect_ezsp from .firmware import FirmwareImage from .gecko_bootloader import GeckoBootloaderProtocol, NoFirmwareError -from .gpio import send_gpio_pattern +from .gpio import find_gpiochip_by_label, send_gpio_pattern from .spinel import SpinelProtocol from .xmodemcrc import BLOCK_SIZE as XMODEM_BLOCK_SIZE @@ -69,6 +69,8 @@ async def enter_bootloader_reset(self, target): _LOGGER.info(f"Triggering {target.value} bootloader") if target in GPIO_CONFIGS.keys(): config = GPIO_CONFIGS[target] + if "chip" not in config.keys(): + config["chip"] = await find_gpiochip_by_label(config["chip_name"]) await send_gpio_pattern( config["chip"], config["pin_states"], config["toggle_delay"] ) diff --git a/universal_silabs_flasher/gpio.py b/universal_silabs_flasher/gpio.py index 899ae44..f603e92 100644 --- a/universal_silabs_flasher/gpio.py +++ b/universal_silabs_flasher/gpio.py @@ -1,10 +1,14 @@ from __future__ import annotations import asyncio +from os import scandir import time +import typing try: import gpiod + + is_gpiod_v1 = hasattr(gpiod.chip, "OPEN_BY_PATH") except ImportError: gpiod = None @@ -15,7 +19,7 @@ def _send_gpio_pattern( ) -> None: raise NotImplementedError("GPIO not supported on this platform") -elif hasattr(gpiod.chip, "OPEN_BY_PATH"): +elif is_gpiod_v1: # gpiod <= 1.5.4 def _send_gpio_pattern( chip: str, pin_states: dict[int, list[bool]], toggle_delay: float @@ -82,6 +86,39 @@ def _send_gpio_pattern( ) +def _generate_gpio_chips() -> typing.Iterable[str]: + for entry in scandir("/dev/"): + if is_gpiod_v1: + if entry.name.startswith("gpiochip"): + yield entry.path + else: + if gpiod.is_gpiochip_device(entry.path): + yield entry.path + + +def _find_gpiochip_by_label(label: str) -> str: + for path in _generate_gpio_chips(): + try: + if is_gpiod_v1: + chip = gpiod.chip(path, gpiod.chip.OPEN_BY_PATH) + if chip.label == label: + return path + else: + with gpiod.Chip(path) as chip: + if chip.get_info().label == label: + return path + except PermissionError: + pass + raise RuntimeError("No matching gpiochip device found") + + +async def find_gpiochip_by_label(label: str) -> str: + result = await asyncio.get_running_loop().run_in_executor( + None, _find_gpiochip_by_label, label + ) + return result + + async def send_gpio_pattern( chip: str, pin_states: dict[int, list[bool]], toggle_delay: float ) -> None: From b6a10d9dd2829ba2ad28b7e73025030912364628 Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sat, 27 Jan 2024 13:40:35 +1100 Subject: [PATCH 2/6] Add bootloader reset for SLZB-07 --- README.md | 2 +- universal_silabs_flasher/const.py | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bd1c32..fbe4ac4 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ Options: --ezsp-baudrate NUMBERS [default: 115200] --spinel-baudrate NUMBERS [default: 460800] --probe-method TEXT [default: bootloader, cpc, ezsp, spinel] - --bootloader-reset [yellow|ihost|sonoff] + --bootloader-reset [yellow|ihost|slzb07|sonoff] --help Show this message and exit. Commands: diff --git a/universal_silabs_flasher/const.py b/universal_silabs_flasher/const.py index 89885bf..799f84e 100644 --- a/universal_silabs_flasher/const.py +++ b/universal_silabs_flasher/const.py @@ -48,6 +48,7 @@ class ApplicationType(enum.Enum): class ResetTarget(enum.Enum): YELLOW = "yellow" IHOST = "ihost" + SLZB07 = "slzb07" SONOFF = "sonoff" @@ -68,4 +69,12 @@ class ResetTarget(enum.Enum): }, "toggle_delay": 0.1, }, + ResetTarget.SLZB07: { + "chip_name": "cp210x", + "pin_states": { + 5: [True, False, False, True], + 4: [True, False, True, True], + }, + "toggle_delay": 0.1, + }, } From 34541a40925fac3d43958db00d1f18aa886d98f5 Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sun, 11 Feb 2024 16:38:56 +1100 Subject: [PATCH 3/6] Add message to avoid other USB devices --- universal_silabs_flasher/flasher.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/universal_silabs_flasher/flasher.py b/universal_silabs_flasher/flasher.py index db617c1..30e5ba9 100644 --- a/universal_silabs_flasher/flasher.py +++ b/universal_silabs_flasher/flasher.py @@ -70,6 +70,10 @@ async def enter_bootloader_reset(self, target): if target in GPIO_CONFIGS.keys(): config = GPIO_CONFIGS[target] if "chip" not in config.keys(): + _LOGGER.warning( + f"When using {target.value} bootloader reset " + + "ensure no other CP2102 USB serial devices are connected." + ) config["chip"] = await find_gpiochip_by_label(config["chip_name"]) await send_gpio_pattern( config["chip"], config["pin_states"], config["toggle_delay"] From deb4c8ae32d2eef035604fb3492827b5fb187b51 Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sun, 30 Jun 2024 14:33:19 +1000 Subject: [PATCH 4/6] De-assert control signals when probing Spinel Some USB dongles have DTR/RTS directly connected for bootloader reset. If these lines are asserted on connection (which they are on Linux), device will fail to startup. --- universal_silabs_flasher/spinel.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/universal_silabs_flasher/spinel.py b/universal_silabs_flasher/spinel.py index 4d2a2c9..1031319 100644 --- a/universal_silabs_flasher/spinel.py +++ b/universal_silabs_flasher/spinel.py @@ -6,6 +6,7 @@ import typing import async_timeout +from serial_asyncio import SerialTransport import zigpy.types from .common import SerialProtocol, Version, crc16_kermit @@ -109,6 +110,14 @@ def __init__(self) -> None: self._transaction_id: int = 1 self._pending_frames: dict[int, asyncio.Future] = {} + def connection_made(self, transport: SerialTransport) -> None: + super().connection_made(transport) + + # de-assert DTR/RTS, some dongles use these to reset device + if transport is not None: + transport.serial.dtr = False + transport.serial.rts = False + def data_received(self, data: bytes) -> None: super().data_received(data) From 16a188a52082252935468fecaa8004cc6de68bb8 Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sun, 30 Jun 2024 14:11:17 +1000 Subject: [PATCH 5/6] Parse out vendor field from Spinel response --- universal_silabs_flasher/flasher.py | 8 ++++++-- universal_silabs_flasher/spinel.py | 6 +++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/universal_silabs_flasher/flasher.py b/universal_silabs_flasher/flasher.py index 30e5ba9..da0d3a7 100644 --- a/universal_silabs_flasher/flasher.py +++ b/universal_silabs_flasher/flasher.py @@ -36,6 +36,7 @@ class ProbeResult: version: Version | None continue_probing: bool baudrate: int + vendor: str | None = dataclasses.field(default=None) class Flasher: @@ -153,10 +154,11 @@ async def probe_ezsp(self, baudrate: int) -> ProbeResult: async def probe_spinel(self, baudrate: int) -> ProbeResult: async with self._connect_spinel(baudrate) as spinel: - version = await spinel.probe() + version, vendor = await spinel.probe() return ProbeResult( version=version, + vendor=vendor, baudrate=baudrate, continue_probing=False, ) @@ -219,6 +221,7 @@ async def probe_app_type( self.app_type = probe_method self.app_version = result.version self.app_baudrate = result.baudrate + self.app_vendor = result.vendor.strip() if result.vendor else "" break else: if bootloader_probe and self._reset_target: @@ -235,8 +238,9 @@ async def probe_app_type( raise RuntimeError("Failed to probe running application type") _LOGGER.info( - "Detected %s, version %s at %s baudrate (bootloader baudrate %s)", + "Detected %s, %s version %s at %s baudrate (bootloader baudrate %s)", self.app_type, + self.app_vendor, self.app_version, self.app_baudrate, self.bootloader_baudrate, diff --git a/universal_silabs_flasher/spinel.py b/universal_silabs_flasher/spinel.py index 1031319..5f2cb2f 100644 --- a/universal_silabs_flasher/spinel.py +++ b/universal_silabs_flasher/spinel.py @@ -248,7 +248,7 @@ async def send_command( return await self.send_frame(frame, **kwargs) - async def probe(self) -> Version: + async def probe(self) -> tuple[Version, str]: rsp = await self.send_command( CommandID.PROP_VALUE_GET, PropertyID.NCP_VERSION.serialize(), @@ -261,9 +261,9 @@ async def probe(self) -> Version: version = version_string.rstrip(b"\x00").decode("ascii") # We strip off the date code to get something reasonably stable - short_version, _ = version.split(";", 1) + short_version, vendor, _ = version.split(";", 2) - return Version(short_version) + return Version(short_version), vendor async def enter_bootloader(self) -> None: await self.send_command( From 2e20c3b66639da60b624d500d9598cc6fc9095e6 Mon Sep 17 00:00:00 2001 From: Tim Lunn Date: Sun, 30 Jun 2024 14:56:20 +1000 Subject: [PATCH 6/6] Bail out if probed a thread adapter that isnt silabs --- universal_silabs_flasher/flasher.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/universal_silabs_flasher/flasher.py b/universal_silabs_flasher/flasher.py index da0d3a7..33d57b6 100644 --- a/universal_silabs_flasher/flasher.py +++ b/universal_silabs_flasher/flasher.py @@ -260,6 +260,11 @@ async def enter_bootloader(self) -> None: elif self.app_type is ApplicationType.SPINEL: async with self._connect_spinel(self.app_baudrate) as spinel: async with async_timeout.timeout(PROBE_TIMEOUT): + if self.app_vendor and "EFR32" not in self.app_vendor: + raise RuntimeError( + "Flashing Thread firmware is only supported on " + "EFR32 devices" + ) await spinel.enter_bootloader() elif self.app_type is ApplicationType.EZSP: async with self._connect_ezsp(self.app_baudrate) as ezsp: