Skip to content

Commit

Permalink
Bootloader reset improvements (#51)
Browse files Browse the repository at this point in the history
* Remove duplicate ctx flasher setup

* Add a --bootloader-reset option

* Move gpio config into dict

* Replace individual reset flags with new bootloader-reset option

* Add ihost bootloader reset

* Only run firmware from the bootloader if we have bootloader reset and
other probe methods

Fixes #52

* fix formatting
  • Loading branch information
darkxst authored Dec 23, 2023
1 parent 3dbfc51 commit 4f7fff0
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 53 deletions.
26 changes: 26 additions & 0 deletions universal_silabs_flasher/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,29 @@ class ApplicationType(enum.Enum):
ApplicationType.EZSP: [115200],
ApplicationType.SPINEL: [460800],
}


class ResetTarget(enum.Enum):
YELLOW = "yellow"
IHOST = "ihost"
SONOFF = "sonoff"


GPIO_CONFIGS = {
ResetTarget.YELLOW: {
"chip": "/dev/gpiochip0",
"pin_states": {
24: [True, False, False, True],
25: [True, False, True, True],
},
"toggle_delay": 0.1,
},
ResetTarget.IHOST: {
"chip": "/dev/gpiochip1",
"pin_states": {
27: [True, False, False, True],
26: [True, False, True, True],
},
"toggle_delay": 0.1,
},
}
61 changes: 35 additions & 26 deletions universal_silabs_flasher/flash.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@
import zigpy.types

from .common import CommaSeparatedNumbers, patch_pyserial_asyncio, put_first
from .const import DEFAULT_BAUDRATES, FW_IMAGE_TYPE_TO_APPLICATION_TYPE, ApplicationType
from .const import (
DEFAULT_BAUDRATES,
FW_IMAGE_TYPE_TO_APPLICATION_TYPE,
ApplicationType,
ResetTarget,
)
from .flasher import Flasher
from .gbl import FirmwareImageType, GBLImage
from .xmodemcrc import BLOCK_SIZE as XMODEM_BLOCK_SIZE, ReceiverCancelled
Expand Down Expand Up @@ -134,6 +139,10 @@ def convert(self, value: tuple | str, param: click.Parameter, ctx: click.Context
callback=click_enum_validator_factory(ApplicationType),
show_default=True,
)
@click.option(
"--bootloader-reset",
type=click.Choice([t.value for t in ResetTarget]),
)
@click.pass_context
def main(
ctx: click.Context,
Expand All @@ -145,6 +154,7 @@ def main(
ezsp_baudrate: list[int],
spinel_baudrate: list[int],
probe_method: list[ApplicationType],
bootloader_reset: str | None,
) -> None:
coloredlogs.install(level=LOG_LEVELS[min(len(LOG_LEVELS) - 1, verbose)])

Expand All @@ -156,6 +166,17 @@ def main(
" (see `--help`)"
)

# To maintain some backwards compatibility, make `--device` required only when we
# are actually invoking a command that interacts with a device
if ctx.get_parameter_source(
"device"
) == click.core.ParameterSource.DEFAULT and ctx.invoked_subcommand not in (
dump_gbl_metadata.name
):
# Replicate the "Error: Missing option" traceback
param = next(p for p in ctx.command.params if p.name == "device")
raise click.MissingParameter(ctx=ctx, param=param)

ctx.obj = {
"verbosity": verbose,
"flasher": Flasher(
Expand All @@ -167,29 +188,9 @@ def main(
ApplicationType.SPINEL: spinel_baudrate,
},
probe_methods=probe_method,
bootloader_reset=bootloader_reset,
),
}
# To maintain some backwards compatibility, make `--device` required only when we
# are actually invoking a command that interacts with a device
if ctx.get_parameter_source(
"device"
) == click.core.ParameterSource.DEFAULT and ctx.invoked_subcommand not in (
dump_gbl_metadata.name
):
# Replicate the "Error: Missing option" traceback
param = next(p for p in ctx.command.params if p.name == "device")
raise click.MissingParameter(ctx=ctx, param=param)

ctx.obj["flasher"] = Flasher(
device=device,
baudrates={
ApplicationType.GECKO_BOOTLOADER: bootloader_baudrate,
ApplicationType.CPC: cpc_baudrate,
ApplicationType.EZSP: ezsp_baudrate,
ApplicationType.SPINEL: spinel_baudrate,
},
probe_methods=probe_method,
)


@main.command()
Expand Down Expand Up @@ -316,12 +317,20 @@ async def flash(
flasher._baudrates[app_type] = put_first(
flasher._baudrates[app_type], [metadata.baudrate]
)
# Maintain backward compatibility with the deprecated reset flags
reset_msg = (
"The '%s' flag is deprecated. Use '--bootloader-reset' "
"instead, see --help for details."
)
if yellow_gpio_reset:
flasher._reset_target = ResetTarget.YELLOW
_LOGGER.info(reset_msg, "--yellow-gpio-reset")
elif sonoff_reset:
flasher._reset_target = ResetTarget.SONOFF
_LOGGER.info(reset_msg, "--sonoff-reset")

try:
await flasher.probe_app_type(
yellow_gpio_reset=yellow_gpio_reset,
sonoff_reset=sonoff_reset,
)
await flasher.probe_app_type()
except RuntimeError as e:
raise click.ClickException(str(e)) from e

Expand Down
50 changes: 23 additions & 27 deletions universal_silabs_flasher/flasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import bellows.types

from .common import PROBE_TIMEOUT, SerialProtocol, Version, connect_protocol
from .const import DEFAULT_BAUDRATES, ApplicationType
from .const import DEFAULT_BAUDRATES, GPIO_CONFIGS, ApplicationType, ResetTarget
from .cpc import CPCProtocol
from .emberznet import connect_ezsp
from .gbl import GBLImage
Expand Down Expand Up @@ -42,6 +42,7 @@ def __init__(
ApplicationType.SPINEL,
),
device: str,
bootloader_reset: str,
):
self._baudrates = baudrates
self._probe_methods = probe_methods
Expand All @@ -52,21 +53,21 @@ def __init__(
self.app_baudrate: int | None = None
self.bootloader_baudrate: int | None = None

async def enter_yellow_bootloader(self):
_LOGGER.info("Triggering Yellow bootloader")

await send_gpio_pattern(
chip="/dev/gpiochip0",
pin_states={
24: [True, False, False, True],
25: [True, False, True, True],
},
toggle_delay=0.1,
self._reset_target: ResetTarget | None = (
ResetTarget(bootloader_reset) if bootloader_reset else None
)

async def enter_sonoff_bootloader(self):
_LOGGER.info("Triggering Sonoff bootloader")
async def enter_bootloader_reset(self, target):
_LOGGER.info(f"Triggering {target.value} bootloader")
if target in GPIO_CONFIGS.keys():
config = GPIO_CONFIGS[target]
await send_gpio_pattern(
config["chip"], config["pin_states"], config["toggle_delay"]
)
else:
await self.enter_serial_bootloader()

async def enter_serial_bootloader(self):
baudrate = self._baudrates[ApplicationType.GECKO_BOOTLOADER][0]
async with connect_protocol(self._device, baudrate, SerialProtocol) as sonoff:
serial = sonoff._transport.serial
Expand Down Expand Up @@ -147,26 +148,24 @@ async def probe_spinel(self, baudrate: int) -> ProbeResult:
async def probe_app_type(
self,
types: typing.Iterable[ApplicationType] | None = None,
*,
yellow_gpio_reset: bool = False,
sonoff_reset: bool = False,
) -> None:
if types is None:
types = self._probe_methods

if yellow_gpio_reset:
await self.enter_yellow_bootloader()
elif sonoff_reset:
await self.enter_sonoff_bootloader()
# Reset into bootloader
if self._reset_target:
await self.enter_bootloader_reset(self._reset_target)

bootloader_probe = None

# Only run firmware from the bootloader if we have other probe methods
# Only run firmware from the bootloader if we have bootloader reset and
# other probe methods
only_probe_bootloader = types == [ApplicationType.GECKO_BOOTLOADER]
run_firmware = self._reset_target and not only_probe_bootloader
probe_funcs = {
ApplicationType.GECKO_BOOTLOADER: (
lambda baudrate: self.probe_gecko_bootloader(
run_firmware=(not only_probe_bootloader), baudrate=baudrate
run_firmware=run_firmware, baudrate=baudrate
)
),
ApplicationType.CPC: self.probe_cpc,
Expand Down Expand Up @@ -206,13 +205,10 @@ async def probe_app_type(
self.app_baudrate = result.baudrate
break
else:
if bootloader_probe and (yellow_gpio_reset or sonoff_reset):
if bootloader_probe and self._reset_target:
# We have no valid application image but can still re-enter the
# bootloader
if yellow_gpio_reset:
await self.enter_yellow_bootloader()
elif sonoff_reset:
await self.enter_sonoff_bootloader()
await self.enter_bootloader_reset(self._reset_target)

self.app_type = ApplicationType.GECKO_BOOTLOADER
self.app_version = bootloader_probe.version
Expand Down

0 comments on commit 4f7fff0

Please sign in to comment.