Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(hardware): add progress output to subsystem firmware update process #12059

Merged
merged 11 commits into from
Feb 9, 2023
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -700,16 +700,19 @@ def fw_version(self) -> Optional[str]:
async def update_firmware(self, filename: str, target: OT3SubSystem) -> None:
"""Update the firmware."""
with open(filename, "r") as f:
await firmware_update.run_update(
update_details = {
sub_system_to_node_id(target): f,
}
updater = firmware_update.RunUpdate(
messenger=self._messenger,
node_id=sub_system_to_node_id(target),
hex_file=f,
update_details=update_details,
# TODO (amit, 2022-04-05): Fill in retry_count and timeout_seconds from
# config values.
retry_count=3,
timeout_seconds=20,
erase=True,
)
await updater.run_updates()

def engaged_axes(self) -> OT3AxisMap[bool]:
"""Get engaged axes."""
Expand Down
5 changes: 2 additions & 3 deletions hardware/opentrons_hardware/firmware_update/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from .downloader import FirmwareUpdateDownloader
from .hex_file import from_hex_file_path, from_hex_file, HexRecordProcessor
from .eraser import FirmwareUpdateEraser
from .run import run_update, run_updates
from .run import RunUpdate

__all__ = [
"FirmwareUpdateDownloader",
Expand All @@ -15,6 +15,5 @@
"from_hex_file_path",
"from_hex_file",
"HexRecordProcessor",
"run_update",
"run_updates",
"RunUpdate",
]
12 changes: 7 additions & 5 deletions hardware/opentrons_hardware/firmware_update/downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
payloads,
fields,
)
from typing import AsyncIterator

logger = logging.getLogger(__name__)

Expand All @@ -34,23 +35,23 @@ async def run(
node_id: NodeId,
hex_processor: HexRecordProcessor,
ack_wait_seconds: float,
) -> None:
) -> AsyncIterator[float]:
"""Download hex record chunks to node.

Args:
node_id: The target node id.
hex_processor: The producer of hex chunks.
ack_wait_seconds: Number of seconds to wait for an ACK
ack_wait_seconds: Number of seconds to wait for an ACK.

Returns:
None
"""
chunks = list(hex_processor.process(fields.FirmwareUpdateDataField.NUM_BYTES))
total_chunks = len(chunks)
with WaitableCallback(self._messenger) as reader:
num_messages = 0
crc32 = 0
for chunk in hex_processor.process(
fields.FirmwareUpdateDataField.NUM_BYTES
):
for chunk in chunks:
logger.debug(
f"Sending chunk {num_messages} to address {chunk.address:x}."
)
Expand All @@ -72,6 +73,7 @@ async def run(

crc32 = binascii.crc32(data, crc32)
num_messages += 1
yield num_messages / total_chunks

# Create and send firmware update complete message.
complete_message = message_definitions.FirmwareUpdateComplete(
Expand Down
209 changes: 123 additions & 86 deletions hardware/opentrons_hardware/firmware_update/run.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""Complete FW updater."""
import logging
import asyncio
from typing import Optional, TextIO, Dict
from typing import Optional, TextIO, Dict, Tuple, AsyncIterator
from .types import FirmwareUpdateStatus, StatusElement

from opentrons_hardware.drivers.can_bus import CanMessenger
from opentrons_hardware.firmware_bindings import NodeId
Expand All @@ -18,98 +19,134 @@

logger = logging.getLogger(__name__)

UpdateDict = Dict[NodeId, TextIO]

async def run_update(
messenger: CanMessenger,
node_id: NodeId,
hex_file: TextIO,
retry_count: int,
timeout_seconds: float,
erase: Optional[bool] = True,
) -> None:
"""Perform a firmware update on a node target.

Args:
messenger: The can messenger to use.
node_id: The node being updated.
hex_file: File containing firmware.
retry_count: Number of times to retry.
timeout_seconds: How much to wait for responses.
erase: Whether to erase flash before updating.

Returns:
None
"""
hex_processor = HexRecordProcessor.from_file(hex_file)

initiator = FirmwareUpdateInitiator(messenger)
downloader = FirmwareUpdateDownloader(messenger)

target = Target(system_node=node_id)

logger.info(f"Initiating FW Update on {target}.")

await initiator.run(
target=target,
retry_count=retry_count,
ready_wait_time_sec=timeout_seconds,
)
if erase:
eraser = FirmwareUpdateEraser(messenger)
logger.info(f"Erasing existing FW Update on {target}.")
await eraser.run(
node_id=target.bootloader_node,
timeout_sec=timeout_seconds,
)
else:
logger.info("Skipping erase step.")

logger.info(f"Downloading FW to {target.bootloader_node}.")
await downloader.run(
node_id=target.bootloader_node,
hex_processor=hex_processor,
ack_wait_seconds=timeout_seconds,
)
class RunUpdate:
"""Class for updating robot microcontroller firmware."""

def __init__(
self,
messenger: CanMessenger,
update_details: UpdateDict,
retry_count: int,
timeout_seconds: float,
erase: Optional[bool] = True,
) -> None:
"""Initialize RunUpdate class.

Args:
messenger: The can messenger to use.
update_details: Dict of nodes to be updated and their firmware files.
retry_count: Number of times to retry.
timeout_seconds: How much to wait for responses.
erase: Whether to erase flash before updating.

Returns:
None
"""
self._messenger = messenger
self._update_details = update_details
self._retry_count = retry_count
self._timeout_seconds = timeout_seconds
self._erase = erase
self._status_dict = {
node_id: (FirmwareUpdateStatus.queued, 0)
for node_id in update_details.keys()
}
self._status_queue: "asyncio.Queue[Tuple[NodeId,StatusElement]]" = (
asyncio.Queue()
)

logger.info(f"Restarting FW on {target.system_node}.")
await messenger.send(
node_id=target.bootloader_node,
message=FirmwareUpdateStartApp(),
)
async def _run_update(
self,
messenger: CanMessenger,
node_id: NodeId,
hex_file: TextIO,
retry_count: int,
timeout_seconds: float,
erase: Optional[bool] = True,
) -> None:
"""Perform a firmware update on a node target."""
hex_processor = HexRecordProcessor.from_file(hex_file)

initiator = FirmwareUpdateInitiator(messenger)
downloader = FirmwareUpdateDownloader(messenger)

UpdateDict = Dict[NodeId, TextIO]
target = Target(system_node=node_id)

logger.info(f"Initiating FW Update on {target}.")
await self._status_queue.put((node_id, (FirmwareUpdateStatus.updating, 0)))

async def run_updates(
messenger: CanMessenger,
update_details: UpdateDict,
retry_count: int,
timeout_seconds: float,
erase: Optional[bool] = True,
) -> None:
"""Perform a firmware update on multiple node targets.

Args:
messenger: The can messenger to use.
update_details: Dict of nodes to be updated and their firmware files.
retry_count: Number of times to retry.
timeout_seconds: How much to wait for responses.
erase: Whether to erase flash before updating.

Returns:
None
"""
tasks = [
run_update(
messenger=messenger,
node_id=node_id,
hex_file=hex_file,
await initiator.run(
target=target,
retry_count=retry_count,
timeout_seconds=timeout_seconds,
erase=erase,
ready_wait_time_sec=timeout_seconds,
)
download_start_progress = 0.1
await self._status_queue.put(
(node_id, (FirmwareUpdateStatus.updating, download_start_progress))
)
for node_id, hex_file in update_details.items()
]

await asyncio.gather(*tasks)
if erase:
eraser = FirmwareUpdateEraser(messenger)
logger.info(f"Erasing existing FW Update on {target}.")
await eraser.run(
node_id=target.bootloader_node,
timeout_sec=timeout_seconds,
)
download_start_progress = 0.2
await self._status_queue.put(
(node_id, (FirmwareUpdateStatus.updating, download_start_progress))
)
else:
logger.info("Skipping erase step.")

logger.info(f"Downloading FW to {target.bootloader_node}.")
async for download_progress in downloader.run(
node_id=target.bootloader_node,
hex_processor=hex_processor,
ack_wait_seconds=timeout_seconds,
):
await self._status_queue.put(
(
node_id,
(
FirmwareUpdateStatus.updating,
download_start_progress
+ (0.9 - download_start_progress) * download_progress,
),
)
)

logger.info(f"Restarting FW on {target.system_node}.")
await messenger.send(
node_id=target.bootloader_node,
message=FirmwareUpdateStartApp(),
)
await self._status_queue.put((node_id, (FirmwareUpdateStatus.done, 1)))

async def run_updates(
self,
) -> AsyncIterator[Tuple[NodeId, StatusElement]]:
"""Perform a firmware update on multiple node targets."""
tasks = [
self._run_update(
messenger=self._messenger,
node_id=node_id,
hex_file=hex_file,
retry_count=self._retry_count,
timeout_seconds=self._timeout_seconds,
erase=self._erase,
)
for node_id, hex_file in self._update_details.items()
]

task = asyncio.create_task(asyncio.gather(*tasks))
while True:
try:
yield await asyncio.wait_for(self._status_queue.get(), 0.25)
except asyncio.TimeoutError:
pass
if task.done():
break
16 changes: 16 additions & 0 deletions hardware/opentrons_hardware/firmware_update/types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""Types for firmware updates."""
from enum import Enum, auto
from typing import Dict, Tuple
from opentrons_hardware.firmware_bindings import NodeId


class FirmwareUpdateStatus(Enum):
"""Firmware Update Status for each Node."""

queued = auto()
updating = auto()
done = auto()


StatusElement = Tuple[FirmwareUpdateStatus, float]
StatusDict = Dict[NodeId, StatusElement]
13 changes: 8 additions & 5 deletions hardware/opentrons_hardware/scripts/update_fw.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from opentrons_hardware.drivers.can_bus import build
from opentrons_hardware.firmware_bindings import NodeId
from opentrons_hardware.firmware_update.run import run_update
from opentrons_hardware.firmware_update.run import RunUpdate
from .can_args import add_can_args, build_settings


Expand Down Expand Up @@ -48,20 +48,23 @@

async def run(args: argparse.Namespace) -> None:
"""Entry point for script."""
target = TARGETS[args.target]
retry_count = args.retry_count
timeout_seconds = args.timeout_seconds
erase = not args.no_erase
update_details = {
TARGETS[args.target]: args.file,
}

async with build.can_messenger(build_settings(args)) as messenger:
await run_update(
updater = RunUpdate(
messenger=messenger,
node_id=target,
hex_file=args.file,
update_details=update_details,
retry_count=retry_count,
timeout_seconds=timeout_seconds,
erase=erase,
)
async for progress in updater.run_updates():
logger.info("%s is %s and %f done", "progress[0], progress[1], progress[2]")

logger.info("Done")

Expand Down
6 changes: 4 additions & 2 deletions hardware/opentrons_hardware/scripts/update_fws.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

from opentrons_hardware.drivers.can_bus import build
from opentrons_hardware.firmware_bindings import NodeId
from opentrons_hardware.firmware_update.run import run_updates
from opentrons_hardware.firmware_update.run import RunUpdate
from .can_args import add_can_args, build_settings


Expand Down Expand Up @@ -58,13 +58,15 @@ async def run(args: argparse.Namespace) -> None:
}

async with build.can_messenger(build_settings(args)) as messenger:
await run_updates(
updater = RunUpdate(
messenger=messenger,
update_details=update_details,
retry_count=retry_count,
timeout_seconds=timeout_seconds,
erase=erase,
)
async for progress in updater.run_updates():
logger.info("%s is %s and %f done", "progress[0], progress[1], progress[2]")
pmoegenburg marked this conversation as resolved.
Show resolved Hide resolved

logger.info("Done")

Expand Down
Loading