Skip to content

Commit 6c1d723

Browse files
authored
Merge pull request #13 from intercreate/feature/intercreate-group
Feature/intercreate group
2 parents c7f3d9a + 06bb599 commit 6c1d723

File tree

6 files changed

+345
-253
lines changed

6 files changed

+345
-253
lines changed

poetry.lock

+249-249
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ source = "git-tag"
1919

2020
[tool.poetry.dependencies]
2121
python = ">=3.10, <3.13"
22-
smpclient = "^1.0.1"
22+
smpclient = "^1.3.1"
2323
typer = {extras = ["all"], version = "^0.9.0"}
2424

2525

smpmgr/common.py

+14-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import asyncio
44
import logging
55
from dataclasses import dataclass, fields
6+
from typing import Type, TypeVar
67

78
import typer
89
from rich.progress import Progress, SpinnerColumn, TextColumn
@@ -14,6 +15,11 @@
1415

1516
logger = logging.getLogger(__name__)
1617

18+
TSMPClient = TypeVar(
19+
"TSMPClient",
20+
bound=SMPClient,
21+
)
22+
1723

1824
@dataclass(frozen=True)
1925
class TransportDefinition:
@@ -27,13 +33,13 @@ class Options:
2733
mtu: int
2834

2935

30-
def get_smpclient(options: Options) -> SMPClient:
31-
"""Return an `SMPClient` to the chosen transport or raise `typer.Exit`."""
36+
def get_custom_smpclient(options: Options, smp_client_cls: Type[TSMPClient]) -> TSMPClient:
37+
"""Return an `SMPClient` subclass to the chosen transport or raise `typer.Exit`."""
3238
if options.transport.port is not None:
3339
logger.info(
3440
f"Initializing SMPClient with the SMPSerialTransport, {options.transport.port=}"
3541
)
36-
return SMPClient(SMPSerialTransport(mtu=options.mtu), options.transport.port)
42+
return smp_client_cls(SMPSerialTransport(mtu=options.mtu), options.transport.port)
3743
else:
3844
typer.echo(
3945
f"A transport option is required; "
@@ -43,6 +49,11 @@ def get_smpclient(options: Options) -> SMPClient:
4349
raise typer.Exit(code=1)
4450

4551

52+
def get_smpclient(options: Options) -> SMPClient:
53+
"""Return an `SMPClient` to the chosen transport or raise `typer.Exit`."""
54+
return get_custom_smpclient(options, SMPClient)
55+
56+
4657
async def connect_with_spinner(smpclient: SMPClient) -> None:
4758
"""Spin while connecting to the SMP Server; raises `typer.Exit` if connection fails."""
4859
with Progress(

smpmgr/main.py

+2
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from smpmgr.image_management import upload_with_progress_bar
2727
from smpmgr.logging import LogLevel, setup_logging
28+
from smpmgr.user import intercreate
2829

2930
logger = logging.getLogger(__name__)
3031

@@ -36,6 +37,7 @@
3637
app: Final = typer.Typer(help="\n".join(HELP_LINES))
3738
app.add_typer(os_management.app)
3839
app.add_typer(image_management.app)
40+
app.add_typer(intercreate.app)
3941

4042

4143
@app.callback(invoke_without_command=True)

smpmgr/user/__init__.py

Whitespace-only changes.

smpmgr/user/intercreate.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""The Intercreate (ic) subcommand group."""
2+
3+
import asyncio
4+
import logging
5+
from io import BufferedReader
6+
from pathlib import Path
7+
from typing import cast
8+
9+
import typer
10+
from rich.progress import (
11+
BarColumn,
12+
DownloadColumn,
13+
Progress,
14+
TextColumn,
15+
TimeRemainingColumn,
16+
TransferSpeedColumn,
17+
)
18+
from smp import header as smphdr
19+
from smp.exceptions import SMPBadStartDelimiter
20+
from smpclient.extensions import intercreate as ic
21+
from typing_extensions import Annotated
22+
23+
from smpmgr.common import Options, connect_with_spinner, get_custom_smpclient
24+
25+
app = typer.Typer(
26+
name="ic", help=f"The Intercreate User Group ({smphdr.GroupId.INTERCREATE.value})"
27+
)
28+
logger = logging.getLogger(__name__)
29+
30+
31+
async def upload_with_progress_bar(
32+
smpclient: ic.ICUploadClient, file: typer.FileBinaryRead | BufferedReader, image: int = 0
33+
) -> None:
34+
"""Animate a progress bar while uploading the data."""
35+
36+
with Progress(
37+
TextColumn("[bold blue]{task.fields[filename]}", justify="right"),
38+
BarColumn(),
39+
"[progress.percentage]{task.percentage:>3.1f}%",
40+
"•",
41+
DownloadColumn(),
42+
"•",
43+
TransferSpeedColumn(),
44+
"•",
45+
TimeRemainingColumn(),
46+
) as progress:
47+
data = file.read()
48+
file.close()
49+
task = progress.add_task("Uploading", total=len(data), filename=file.name, start=True)
50+
try:
51+
async for offset in smpclient.ic_upload(data, image):
52+
progress.update(task, completed=offset)
53+
logger.info(f"Upload {offset=}")
54+
except SMPBadStartDelimiter as e:
55+
progress.stop()
56+
logger.info(f"Bad start delimiter: {e}")
57+
logger.error("Got an unexpected response, is the device an SMP server?")
58+
raise typer.Exit(code=1)
59+
except OSError as e:
60+
logger.error(f"Connection to device lost: {e.__class__.__name__} - {e}")
61+
raise typer.Exit(code=1)
62+
63+
64+
@app.command()
65+
def upload(
66+
ctx: typer.Context,
67+
file: Annotated[Path, typer.Argument(help="Path to binary data")],
68+
image: Annotated[int, typer.Option(help="The image slot to upload to")] = 0,
69+
) -> None:
70+
"""Upload data to custom image slots, like a secondary MCU or external storage."""
71+
72+
smpclient = get_custom_smpclient(cast(Options, ctx.obj), ic.ICUploadClient)
73+
74+
async def f() -> None:
75+
await connect_with_spinner(smpclient)
76+
with open(file, "rb") as f:
77+
await upload_with_progress_bar(smpclient, f, image)
78+
79+
asyncio.run(f())

0 commit comments

Comments
 (0)