micropython-stubber 1.20.0__py3-none-any.whl → 1.20.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. {micropython_stubber-1.20.0.dist-info → micropython_stubber-1.20.2.dist-info}/METADATA +6 -6
  2. {micropython_stubber-1.20.0.dist-info → micropython_stubber-1.20.2.dist-info}/RECORD +58 -51
  3. mpflash/README.md +54 -35
  4. mpflash/libusb_flash.ipynb +203 -203
  5. mpflash/mpflash/add_firmware.py +98 -0
  6. mpflash/mpflash/ask_input.py +106 -114
  7. mpflash/mpflash/cli_download.py +58 -37
  8. mpflash/mpflash/cli_flash.py +77 -35
  9. mpflash/mpflash/cli_group.py +14 -12
  10. mpflash/mpflash/cli_list.py +40 -4
  11. mpflash/mpflash/cli_main.py +20 -8
  12. mpflash/mpflash/common.py +125 -12
  13. mpflash/mpflash/config.py +2 -0
  14. mpflash/mpflash/connected.py +74 -0
  15. mpflash/mpflash/download.py +67 -50
  16. mpflash/mpflash/downloaded.py +9 -9
  17. mpflash/mpflash/flash.py +2 -2
  18. mpflash/mpflash/flash_esp.py +2 -2
  19. mpflash/mpflash/flash_uf2.py +16 -8
  20. mpflash/mpflash/flash_uf2_linux.py +5 -16
  21. mpflash/mpflash/flash_uf2_macos.py +78 -0
  22. mpflash/mpflash/flash_uf2_windows.py +1 -1
  23. mpflash/mpflash/list.py +58 -57
  24. mpflash/mpflash/mpboard_id/__init__.py +37 -44
  25. mpflash/mpflash/mpboard_id/add_boards.py +255 -0
  26. mpflash/mpflash/mpboard_id/board.py +37 -0
  27. mpflash/mpflash/mpboard_id/board_id.py +50 -43
  28. mpflash/mpflash/mpboard_id/board_info.zip +0 -0
  29. mpflash/mpflash/mpboard_id/store.py +42 -0
  30. mpflash/mpflash/mpremoteboard/__init__.py +18 -6
  31. mpflash/mpflash/mpremoteboard/runner.py +12 -12
  32. mpflash/mpflash/uf2disk.py +12 -0
  33. mpflash/mpflash/vendor/basicgit.py +288 -0
  34. mpflash/mpflash/vendor/dfu.py +1 -0
  35. mpflash/mpflash/vendor/versions.py +7 -3
  36. mpflash/mpflash/worklist.py +71 -48
  37. mpflash/poetry.lock +163 -137
  38. mpflash/pyproject.toml +18 -15
  39. stubber/__init__.py +1 -1
  40. stubber/board/createstubs.py +4 -3
  41. stubber/board/createstubs_db.py +5 -7
  42. stubber/board/createstubs_db_min.py +1 -1
  43. stubber/board/createstubs_db_mpy.mpy +0 -0
  44. stubber/board/createstubs_mem.py +6 -7
  45. stubber/board/createstubs_mem_min.py +1 -1
  46. stubber/board/createstubs_mem_mpy.mpy +0 -0
  47. stubber/board/createstubs_min.py +2 -2
  48. stubber/board/createstubs_mpy.mpy +0 -0
  49. stubber/board/modulelist.txt +1 -0
  50. stubber/commands/get_core_cmd.py +7 -6
  51. stubber/commands/get_docstubs_cmd.py +8 -3
  52. stubber/commands/get_frozen_cmd.py +5 -2
  53. stubber/publish/publish.py +18 -7
  54. stubber/utils/makeversionhdr.py +3 -2
  55. stubber/utils/versions.py +2 -1
  56. mpflash/mpflash/mpboard_id/board_info.csv +0 -2213
  57. mpflash/mpflash/mpboard_id/board_info.json +0 -19910
  58. {micropython_stubber-1.20.0.dist-info → micropython_stubber-1.20.2.dist-info}/LICENSE +0 -0
  59. {micropython_stubber-1.20.0.dist-info → micropython_stubber-1.20.2.dist-info}/WHEEL +0 -0
  60. {micropython_stubber-1.20.0.dist-info → micropython_stubber-1.20.2.dist-info}/entry_points.txt +0 -0
@@ -12,23 +12,26 @@ from .logger import make_quiet, set_loglevel
12
12
  def cb_verbose(ctx, param, value):
13
13
  """Callback to set the log level to DEBUG if verbose is set"""
14
14
  if value and not config.quiet:
15
- set_loglevel("DEBUG")
16
15
  config.verbose = True
16
+ if value > 1:
17
+ set_loglevel("TRACE")
18
+ else:
19
+ set_loglevel("DEBUG")
17
20
  else:
18
21
  set_loglevel("INFO")
19
22
  config.verbose = False
20
23
  return value
21
24
 
22
25
 
23
- def cb_ignore(ctx, param, value):
26
+ def cb_interactive(ctx, param, value):
24
27
  if value:
25
- config.ignore_ports = list(value)
28
+ config.interactive = value
26
29
  return value
27
30
 
28
31
 
29
- def cb_interactive(ctx, param, value):
32
+ def cb_test(ctx, param, value):
30
33
  if value:
31
- config.interactive = value
34
+ config.tests = value
32
35
  return value
33
36
 
34
37
 
@@ -64,21 +67,20 @@ def cb_quiet(ctx, param, value):
64
67
  "-V",
65
68
  "--verbose",
66
69
  is_eager=True,
67
- is_flag=True,
70
+ count=True,
68
71
  help="Enables verbose mode.",
69
72
  callback=cb_verbose,
70
73
  )
71
74
  @click.option(
72
- "--ignore",
73
- "-i",
75
+ "--test",
74
76
  is_eager=True,
75
- help="Serial port(s) to ignore. Defaults to MPFLASH_IGNORE.",
76
- callback=cb_ignore,
77
+ help="test a specific feature",
78
+ callback=cb_test,
77
79
  multiple=True,
78
80
  default=[],
79
- envvar="MPFLASH_IGNORE",
81
+ envvar="MPFLASH_TEST",
80
82
  show_default=True,
81
- metavar="SERIALPORT",
83
+ metavar="TEST",
82
84
  )
83
85
  def cli(**kwargs):
84
86
  """mpflash - MicroPython Tool.
@@ -1,11 +1,13 @@
1
1
  import json
2
+ from typing import List
2
3
 
3
4
  import rich_click as click
4
5
  from rich import print
5
6
 
6
7
  from .cli_group import cli
7
- from .list import list_mcus, show_mcus
8
+ from .list import show_mcus
8
9
  from .logger import make_quiet
10
+ from .connected import list_mcus
9
11
 
10
12
 
11
13
  @cli.command("list", help="List the connected MCU boards.")
@@ -18,24 +20,58 @@ from .logger import make_quiet
18
20
  show_default=True,
19
21
  help="""Output in json format""",
20
22
  )
23
+ @click.option(
24
+ "--serial",
25
+ "--serial-port",
26
+ "-s",
27
+ "serial",
28
+ default=["*"],
29
+ multiple=True,
30
+ show_default=True,
31
+ help="Which serial port(s) to list. ",
32
+ metavar="SERIALPORT",
33
+ )
34
+ @click.option(
35
+ "--ignore",
36
+ "-i",
37
+ is_eager=True,
38
+ help="Serial port(s) to ignore. Defaults to MPFLASH_IGNORE.",
39
+ multiple=True,
40
+ default=[],
41
+ envvar="MPFLASH_IGNORE",
42
+ show_default=True,
43
+ metavar="SERIALPORT",
44
+ )
45
+ @click.option(
46
+ "--bluetooth/--no-bluetooth",
47
+ "-b/-nb",
48
+ is_flag=True,
49
+ default=False,
50
+ show_default=True,
51
+ help="""Include bluetooth ports in the list""",
52
+ )
21
53
  @click.option(
22
54
  "--progress/--no-progress",
55
+ # "-p/-np", -p is already used for --port
23
56
  "progress",
24
57
  is_flag=True,
25
58
  default=True,
26
59
  show_default=True,
27
60
  help="""Show progress""",
28
61
  )
29
- def cli_list_mcus(as_json: bool, progress: bool = True):
62
+ def cli_list_mcus(serial: List[str], ignore: List[str], bluetooth: bool, as_json: bool, progress: bool = True) -> int:
30
63
  """List the connected MCU boards, and output in a nice table or json."""
64
+ serial = list(serial)
65
+ ignore = list(ignore)
31
66
  if as_json:
32
67
  # avoid noise in json output
33
68
  make_quiet()
69
+ # TODO? Ask user to select a serialport if [?] is given ?
34
70
 
35
- conn_mcus = list_mcus()
71
+ conn_mcus = list_mcus(ignore=ignore, include=serial, bluetooth=bluetooth)
36
72
  if as_json:
37
73
  print(json.dumps([mcu.__dict__ for mcu in conn_mcus], indent=4))
38
74
  progress = False
39
75
  if progress:
40
76
  show_mcus(conn_mcus, refresh=False)
41
- return conn_mcus
77
+ return 0 if conn_mcus else 1
@@ -2,24 +2,36 @@
2
2
 
3
3
  # import rich_click as click
4
4
 
5
+ import os
6
+
7
+ import click
8
+ from loguru import logger as log
9
+
5
10
  from .cli_download import cli_download
6
11
  from .cli_flash import cli_flash_board
7
12
  from .cli_group import cli
8
13
  from .cli_list import cli_list_mcus
9
14
 
10
- # from loguru import logger as log
11
-
12
15
 
13
16
  def mpflash():
14
- cli.add_command(cli_flash_board)
15
17
  cli.add_command(cli_list_mcus)
16
18
  cli.add_command(cli_download)
19
+ cli.add_command(cli_flash_board)
20
+
17
21
  # cli(auto_envvar_prefix="MPFLASH")
18
- try:
19
- exit(cli())
20
- except AttributeError as e:
21
- print(f"Error: {e}")
22
- exit(-1)
22
+ if False and os.environ.get("COMPUTERNAME") == "JOSVERL-S4":
23
+ # intentional less error suppression on dev machine
24
+ result = cli(standalone_mode=False)
25
+ else:
26
+ try:
27
+ result = cli(standalone_mode=False)
28
+ exit(result)
29
+ except AttributeError as e:
30
+ log.error(f"Error: {e}")
31
+ exit(-1)
32
+ except click.exceptions.ClickException as e:
33
+ log.error(f"Error: {e}")
34
+ exit(-2)
23
35
 
24
36
 
25
37
  if __name__ == "__main__":
mpflash/mpflash/common.py CHANGED
@@ -1,11 +1,16 @@
1
+ import fnmatch
1
2
  import os
2
- import time
3
- from typing import TypedDict
3
+ import sys
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import List, Optional, Union
4
7
 
5
8
  from github import Auth, Github
6
- from rich.progress import track
9
+ from serial.tools import list_ports
10
+ from serial.tools.list_ports_common import ListPortInfo
11
+
12
+ from .logger import log
7
13
 
8
- from mpflash.errors import MPFlashError
9
14
  # from mpflash.mpremoteboard import MPRemoteBoard
10
15
 
11
16
  PORT_FWTYPES = {
@@ -14,6 +19,7 @@ PORT_FWTYPES = {
14
19
  "esp8266": [".bin"],
15
20
  "rp2": [".uf2"],
16
21
  "samd": [".uf2"],
22
+ # below this not yet implemented / tested
17
23
  "mimxrt": [".hex"],
18
24
  "nrf": [".uf2"],
19
25
  "renesas-ra": [".hex"],
@@ -28,11 +34,118 @@ PAT = os.environ.get("GITHUB_TOKEN") or PAT_NO_ACCESS
28
34
  GH_CLIENT = Github(auth=Auth.Token(PAT))
29
35
 
30
36
 
31
- class FWInfo(TypedDict):
32
- filename: str
33
- port: str
34
- board: str
35
- variant: str
36
- preview: bool
37
- version: str
38
- build: str
37
+ @dataclass
38
+ class FWInfo:
39
+ """
40
+ Downloaded Firmware information
41
+ is somewhat related to the BOARD class in the mpboard_id module
42
+ """
43
+
44
+ port: str # MicroPython port
45
+ board: str # MicroPython board
46
+ filename: str = field(default="") # relative filename of the firmware image
47
+ firmware: str = field(default="") # url or path to original firmware image
48
+ variant: str = field(default="") # MicroPython variant
49
+ preview: bool = field(default=False) # True if the firmware is a preview version
50
+ version: str = field(default="") # MicroPython version
51
+ url: str = field(default="") # url to the firmware image download folder
52
+ build: str = field(default="0") # The build = number of commits since the last release
53
+ ext: str = field(default="") # the file extension of the firmware
54
+ family: str = field(default="micropython") # The family of the firmware
55
+ custom: bool = field(default=False) # True if the firmware is a custom build
56
+ description: str = field(default="") # Description used by this firmware (custom only)
57
+
58
+ def to_dict(self) -> dict:
59
+ """Convert the object to a dictionary"""
60
+ return self.__dict__
61
+
62
+ @classmethod
63
+ def from_dict(cls, data: dict) -> "FWInfo":
64
+ """Create a FWInfo object from a dictionary"""
65
+ # add missing keys
66
+ if "ext" not in data:
67
+ data["ext"] = Path(data["firmware"]).suffix
68
+ if "family" not in data:
69
+ data["family"] = "micropython"
70
+ return cls(**data)
71
+
72
+
73
+ @dataclass
74
+ class Params:
75
+ """Common parameters for downloading and flashing firmware"""
76
+
77
+ ports: List[str] = field(default_factory=list)
78
+ boards: List[str] = field(default_factory=list)
79
+ versions: List[str] = field(default_factory=list)
80
+ fw_folder: Path = Path()
81
+ serial: List[str] = field(default_factory=list)
82
+ ignore: List[str] = field(default_factory=list)
83
+
84
+
85
+ @dataclass
86
+ class DownloadParams(Params):
87
+ """Parameters for downloading firmware"""
88
+
89
+ clean: bool = False
90
+ force: bool = False
91
+
92
+
93
+ @dataclass
94
+ class FlashParams(Params):
95
+ """Parameters for flashing a board"""
96
+
97
+ erase: bool = True
98
+ bootloader: bool = True
99
+ cpu: str = ""
100
+
101
+
102
+ ParamType = Union[DownloadParams, FlashParams]
103
+
104
+
105
+ def filtered_comports(
106
+ ignore: Optional[List[str]] = None,
107
+ include: Optional[List[str]] = None,
108
+ bluetooth: bool = False,
109
+ ) -> List[ListPortInfo]: # sourcery skip: assign-if-exp
110
+ """
111
+ Get a list of filtered comports.
112
+ """
113
+ if not ignore:
114
+ ignore = []
115
+ elif not isinstance(ignore, list): # type: ignore
116
+ ignore = list(ignore)
117
+ if not include:
118
+ include = ["*"]
119
+ elif not isinstance(include, list): # type: ignore
120
+ include = list(include)
121
+
122
+ # remove ports that are to be ignored
123
+ log.trace(f"{include=}, {ignore=}, {bluetooth=}")
124
+ comports = [p for p in list_ports.comports() if not any(fnmatch.fnmatch(p.device, i) for i in ignore)]
125
+ log.trace(f"comports: {[p.device for p in comports]}")
126
+ # remove bluetooth ports
127
+
128
+ if include != ["*"]:
129
+ # if there are explicit ports to include, add them to the list
130
+ explicit = [p for p in list_ports.comports() if any(fnmatch.fnmatch(p.device, i) for i in include)]
131
+ log.trace(f"explicit: {[p.device for p in explicit]}")
132
+ if ignore == []:
133
+ # if nothing to ignore, just use the explicit list as a sinple sane default
134
+ comports = explicit
135
+ else:
136
+ # if there are ports to ignore, add the explicit list to the filtered list
137
+ comports = list(set(explicit) | set(comports))
138
+ if not bluetooth:
139
+ # filter out bluetooth ports
140
+ comports = [p for p in comports if "bluetooth" not in p.description.lower()]
141
+ comports = [p for p in comports if "BTHENUM" not in p.hwid]
142
+ if sys.platform == "darwin":
143
+ comports = [p for p in comports if ".Bluetooth" not in p.device]
144
+ log.trace(f"no Bluetooth: {[p.device for p in comports]}")
145
+ log.debug(f"filtered_comports: {[p.device for p in comports]}")
146
+ # sort
147
+ if sys.platform == "win32":
148
+ # Windows sort of comports by number - but fallback to device name
149
+ return sorted(comports, key=lambda x: int(x.device.split()[0][3:]) if x.device.split()[0][3:].isdigit() else x)
150
+ # sort by device name
151
+ return sorted(comports, key=lambda x: x.device)
mpflash/mpflash/config.py CHANGED
@@ -14,6 +14,8 @@ class MPtoolConfig:
14
14
  ignore_ports: List[str] = []
15
15
  interactive: bool = True
16
16
  firmware_folder: Path = platformdirs.user_downloads_path() / "firmware"
17
+ # test options specified on the commandline
18
+ tests: List[str] = []
17
19
 
18
20
 
19
21
  config = MPtoolConfig()
@@ -0,0 +1,74 @@
1
+ from typing import List, Tuple
2
+
3
+ from rich import print
4
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
5
+ from rich.table import Column
6
+
7
+ from mpflash.mpremoteboard import MPRemoteBoard
8
+
9
+ from .common import filtered_comports
10
+
11
+
12
+ def connected_ports_boards(
13
+ *, include: List[str], ignore: List[str]
14
+ ) -> Tuple[List[str], List[str], List[MPRemoteBoard]]:
15
+ """
16
+ Returns a tuple containing lists of unique ports and boards from the connected MCUs.
17
+ Boards that are physically connected, but give no tangible response are ignored.
18
+
19
+ Returns:
20
+ A tuple containing three lists:
21
+ - A list of unique ports where MCUs are connected.
22
+ - A list of unique board names of the connected MCUs.
23
+ - A list of MPRemoteBoard instances of the connected MCUs.
24
+ """
25
+ mpr_boards = [b for b in list_mcus(include=include, ignore=ignore) if b.connected]
26
+ ports = list({b.port for b in mpr_boards})
27
+ boards = list({b.board for b in mpr_boards})
28
+ return (ports, boards, mpr_boards)
29
+
30
+
31
+ # #########################################################################################################
32
+ rp_spinner = SpinnerColumn(finished_text="✅")
33
+ rp_text = TextColumn("{task.description} {task.fields[device]}", table_column=Column())
34
+ rp_bar = BarColumn(bar_width=None, table_column=Column())
35
+
36
+
37
+ def list_mcus(*, ignore: List[str], include: List[str], bluetooth: bool = False):
38
+ """
39
+ Retrieves information about connected microcontroller boards.
40
+
41
+ Returns:
42
+ List[MPRemoteBoard]: A list of MPRemoteBoard instances with board information.
43
+ Raises:
44
+ ConnectionError: If there is an error connecting to a board.
45
+ """
46
+ # conn_mcus = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards(bluetooth) if sp not in config.ignore_ports]
47
+
48
+ comports = filtered_comports(
49
+ ignore=ignore,
50
+ include=include,
51
+ bluetooth=bluetooth,
52
+ )
53
+ conn_mcus = [MPRemoteBoard(c.device) for c in comports]
54
+
55
+ # a lot of boilerplate to show a progress bar with the comport currently scanned
56
+ # low update rate to facilitate screen readers/narration
57
+ with Progress(rp_spinner, rp_text, rp_bar, TimeElapsedColumn(), refresh_per_second=2) as progress:
58
+ tsk_scan = progress.add_task("[green]Scanning", visible=False, total=None)
59
+ progress.tasks[tsk_scan].fields["device"] = "..."
60
+ progress.tasks[tsk_scan].visible = True
61
+ progress.start_task(tsk_scan)
62
+ try:
63
+ for mcu in conn_mcus:
64
+ progress.update(tsk_scan, device=mcu.serialport.replace("/dev/", ""))
65
+ try:
66
+ mcu.get_mcu_info()
67
+ except ConnectionError as e:
68
+ print(f"Error: {e}")
69
+ continue
70
+ finally:
71
+ # transient
72
+ progress.stop_task(tsk_scan)
73
+ progress.tasks[tsk_scan].visible = False
74
+ return conn_mcus
@@ -18,8 +18,11 @@ from bs4 import BeautifulSoup
18
18
  from loguru import logger as log
19
19
  from rich.progress import track
20
20
 
21
- from mpflash.common import PORT_FWTYPES
21
+ from mpflash.common import PORT_FWTYPES, FWInfo
22
+ from mpflash.errors import MPFlashError
23
+ from mpflash.mpboard_id import get_known_ports
22
24
 
25
+ # avoid conflict with the ujson used by MicroPython
23
26
  jsonlines.ujson = None # type: ignore
24
27
  # #########################################################################################################
25
28
 
@@ -49,6 +52,10 @@ def get_board_urls(page_url: str) -> List[Dict[str, str]]:
49
52
 
50
53
  Args:
51
54
  page_url (str): The url of the page to get the board urls from.
55
+
56
+ Returns:
57
+ List[Dict[str, str]]: A list of dictionaries containing the board name and url.
58
+
52
59
  """
53
60
  downloads_html = get_page(page_url)
54
61
  soup = BeautifulSoup(downloads_html, "html.parser")
@@ -87,14 +94,10 @@ def board_firmware_urls(board_url: str, base_url: str, ext: str) -> List[str]:
87
94
  return links
88
95
 
89
96
 
90
- # type alias for the firmware info
91
- FirmwareInfo = Dict[str, str]
92
-
93
-
94
97
  # boards we are interested in ( this avoids getting a lot of boards we don't care about)
95
98
  # The first run takes ~60 seconds to run for 4 ports , all boards
96
99
  # so it makes sense to cache the results and skip boards as soon as possible
97
- def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FirmwareInfo]:
100
+ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FWInfo]:
98
101
  """
99
102
  Retrieves a list of firmware information for the specified ports and boards.
100
103
 
@@ -104,57 +107,68 @@ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[Firmwar
104
107
  clean (bool): A flag indicating whether to perform a clean retrieval.
105
108
 
106
109
  Returns:
107
- List[FirmwareInfo]: A list of firmware information for the specified ports and boards.
110
+ List[FWInfo]: A list of firmware information for the specified ports and boards.
108
111
 
109
112
  """
110
- board_urls: List[FirmwareInfo] = []
113
+ board_urls: List[FWInfo] = []
114
+ if ports is None:
115
+ ports = get_known_ports()
111
116
  for port in ports:
112
117
  download_page_url = f"{MICROPYTHON_ORG_URL}download/?port={port}"
113
- _urls = get_board_urls(download_page_url)
118
+ urls = get_board_urls(download_page_url)
114
119
  # filter out boards we don't care about
115
- _urls = [board for board in _urls if board["board"] in boards]
120
+ urls = [board for board in urls if board["board"] in boards]
116
121
  # add the port to the board urls
117
- for board in _urls:
122
+ for board in urls:
118
123
  board["port"] = port
119
124
 
120
- for board in track(_urls, description=f"Checking {port} download pages", transient=True):
125
+ for board in track(urls, description=f"Checking {port} download pages", transient=True, refresh_per_second=2):
121
126
  # add a board to the list for each firmware found
122
- firmwares = []
127
+ firmware_urls: List[str] = []
123
128
  for ext in PORT_FWTYPES[port]:
124
- firmwares += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
125
-
126
- for _url in firmwares:
129
+ firmware_urls += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
130
+ for _url in firmware_urls:
127
131
  board["firmware"] = _url
128
- board["preview"] = "preview" in _url # type: ignore
129
- if ver_match := re.search(RE_VERSION_PREVIEW, _url):
130
- board["version"] = ver_match[1]
131
- else:
132
- board["version"] = ""
133
- if "preview." in board["version"]:
134
- board["build"] = board["version"].split("preview.")[-1]
135
- else:
136
- board["build"] = "0"
137
132
  fname = Path(board["firmware"]).name
138
133
  if clean:
139
134
  # remove date from firmware name
140
135
  fname = re.sub(RE_DATE, "-", fname)
141
136
  # remove hash from firmware name
142
137
  fname = re.sub(RE_HASH, ".", fname)
143
- board["filename"] = fname
144
- board["ext"] = Path(board["firmware"]).suffix
145
- board["variant"] = board["filename"].split("-v")[0] if "-v" in board["filename"] else ""
146
- board_urls.append(board.copy())
138
+ fw_info = FWInfo(
139
+ filename=fname,
140
+ port=port,
141
+ board=board["board"],
142
+ preview="preview" in _url,
143
+ firmware=_url,
144
+ version="",
145
+ )
146
+ # board["firmware"] = _url
147
+ # board["preview"] = "preview" in _url # type: ignore
148
+ if ver_match := re.search(RE_VERSION_PREVIEW, _url):
149
+ fw_info.version = ver_match[1]
150
+ # else:
151
+ # board.$1= ""
152
+ if "preview." in fw_info.version:
153
+ fw_info.build = fw_info.version.split("preview.")[-1]
154
+ else:
155
+ fw_info.build = "0"
156
+
157
+ fw_info.ext = Path(fw_info.firmware).suffix
158
+ fw_info.variant = fw_info.filename.split("-v")[0] if "-v" in fw_info.filename else ""
159
+
160
+ board_urls.append(fw_info)
147
161
  return board_urls
148
162
 
149
163
 
150
- def key_fw_ver_pre_ext_bld(x: FirmwareInfo):
164
+ def key_fw_ver_pre_ext_bld(x: FWInfo):
151
165
  "sorting key for the retrieved board urls"
152
- return x["variant"], x["version"], x["preview"], x["ext"], x["build"]
166
+ return x.variant, x.version, x.preview, x.ext, x.build
153
167
 
154
168
 
155
- def key_fw_var_pre_ext(x: FirmwareInfo):
169
+ def key_fw_var_pre_ext(x: FWInfo):
156
170
  "Grouping key for the retrieved board urls"
157
- return x["variant"], x["preview"], x["ext"]
171
+ return x.variant, x.preview, x.ext
158
172
 
159
173
 
160
174
  def download_firmwares(
@@ -165,14 +179,15 @@ def download_firmwares(
165
179
  *,
166
180
  force: bool = False,
167
181
  clean: bool = True,
168
- ):
182
+ ) -> int:
169
183
  skipped = downloaded = 0
170
184
  if versions is None:
171
185
  versions = []
186
+
172
187
  unique_boards = get_firmware_list(ports, boards, versions, clean)
173
188
 
174
189
  for b in unique_boards:
175
- log.debug(b["filename"])
190
+ log.debug(b.filename)
176
191
  # relevant
177
192
 
178
193
  log.info(f"Found {len(unique_boards)} relevant unique firmwares")
@@ -181,25 +196,26 @@ def download_firmwares(
181
196
 
182
197
  with jsonlines.open(firmware_folder / "firmware.jsonl", "a") as writer:
183
198
  for board in unique_boards:
184
- filename = firmware_folder / board["port"] / board["filename"]
199
+ filename = firmware_folder / board.port / board.filename
185
200
  filename.parent.mkdir(exist_ok=True)
186
201
  if filename.exists() and not force:
187
202
  skipped += 1
188
203
  log.debug(f" {filename} already exists, skip download")
189
204
  continue
190
- log.info(f"Downloading {board['firmware']}")
205
+ log.info(f"Downloading {board.firmware}")
191
206
  log.info(f" to {filename}")
192
207
  try:
193
- r = requests.get(board["firmware"], allow_redirects=True)
208
+ r = requests.get(board.firmware, allow_redirects=True)
194
209
  with open(filename, "wb") as fw:
195
210
  fw.write(r.content)
196
- board["filename"] = str(filename.relative_to(firmware_folder))
211
+ board.filename = str(filename.relative_to(firmware_folder))
197
212
  except requests.RequestException as e:
198
213
  log.exception(e)
199
214
  continue
200
- writer.write(board)
215
+ writer.write(board.to_dict())
201
216
  downloaded += 1
202
217
  log.info(f"Downloaded {downloaded} firmwares, skipped {skipped} existing files.")
218
+ return downloaded + skipped
203
219
 
204
220
 
205
221
  def get_firmware_list(ports: List[str], boards: List[str], versions: List[str], clean: bool):
@@ -213,7 +229,7 @@ def get_firmware_list(ports: List[str], boards: List[str], versions: List[str],
213
229
  clean : A flag indicating whether to perform a clean check.
214
230
 
215
231
  Returns:
216
- List[FirmwareInfo]: A list of unique firmware information.
232
+ List[FWInfo]: A list of unique firmware information.
217
233
 
218
234
  """
219
235
 
@@ -225,12 +241,12 @@ def get_firmware_list(ports: List[str], boards: List[str], versions: List[str],
225
241
  relevant = [
226
242
  board
227
243
  for board in board_urls
228
- if board["board"] in boards and (board["version"] in versions or board["preview"] and preview)
244
+ if board.board in boards and (board.version in versions or board.preview and preview)
229
245
  # and b["port"] in ["esp32", "rp2"]
230
246
  ]
231
247
  log.debug(f"Matching firmwares: {len(relevant)}")
232
248
  # select the unique boards
233
- unique_boards: List[FirmwareInfo] = []
249
+ unique_boards: List[FWInfo] = []
234
250
  for _, g in itertools.groupby(relevant, key=key_fw_var_pre_ext):
235
251
  # list is aleady sorted by build so we can just get the last item
236
252
  sub_list = list(g)
@@ -246,7 +262,7 @@ def download(
246
262
  versions: List[str],
247
263
  force: bool,
248
264
  clean: bool,
249
- ):
265
+ ) -> int:
250
266
  """
251
267
  Downloads firmware files based on the specified destination, ports, boards, versions, force flag, and clean flag.
252
268
 
@@ -259,19 +275,20 @@ def download(
259
275
  clean : A flag indicating whether to perform a clean download.
260
276
 
261
277
  Returns:
262
- None
278
+ int: The number of downloaded firmware files.
263
279
 
264
280
  Raises:
265
- SystemExit: If no boards are found or specified.
281
+ MPFlashError : If no boards are found or specified.
266
282
 
267
283
  """
268
284
  if not boards:
269
285
  log.critical("No boards found, please connect a board or specify boards to download firmware for.")
270
- exit(1)
286
+ raise MPFlashError("No boards found")
271
287
  # versions = [clean_version(v, drop_v=True) for v in versions] # remove leading v from version
272
288
  try:
273
289
  destination.mkdir(exist_ok=True, parents=True)
274
290
  except (PermissionError, FileNotFoundError) as e:
275
- log.critical(f"Could not create folder {destination}\n{e}")
276
- exit(1)
277
- download_firmwares(destination, ports, boards, versions, force=force, clean=clean)
291
+ log.critical(f"Could not create folder {destination}")
292
+ raise MPFlashError(f"Could not create folder {destination}") from e
293
+
294
+ return download_firmwares(destination, ports, boards, versions, force=force, clean=clean)