mpflash 0.7.7__py3-none-any.whl → 0.8.1__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.
mpflash/cli_group.py CHANGED
@@ -3,8 +3,6 @@ Main entry point for the CLI group.
3
3
  Additional comands are added in the submodules.
4
4
  """
5
5
 
6
- import sys
7
-
8
6
  import rich_click as click
9
7
 
10
8
  from .config import config
@@ -14,22 +12,17 @@ from .logger import make_quiet, set_loglevel
14
12
  def cb_verbose(ctx, param, value):
15
13
  """Callback to set the log level to DEBUG if verbose is set"""
16
14
  if value and not config.quiet:
17
- set_loglevel("DEBUG")
18
15
  config.verbose = True
16
+ if value > 1:
17
+ set_loglevel("TRACE")
18
+ else:
19
+ set_loglevel("DEBUG")
19
20
  else:
20
21
  set_loglevel("INFO")
21
22
  config.verbose = False
22
23
  return value
23
24
 
24
25
 
25
- def cb_ignore(ctx, param, value):
26
- if value:
27
- config.ignore_ports = list(value)
28
- if sys.platform == "win32":
29
- config.ignore_ports = [port.upper() for port in config.ignore_ports]
30
- return value
31
-
32
-
33
26
  def cb_interactive(ctx, param, value):
34
27
  if value:
35
28
  config.interactive = value
@@ -74,22 +67,10 @@ def cb_quiet(ctx, param, value):
74
67
  "-V",
75
68
  "--verbose",
76
69
  is_eager=True,
77
- is_flag=True,
70
+ count=True,
78
71
  help="Enables verbose mode.",
79
72
  callback=cb_verbose,
80
73
  )
81
- @click.option(
82
- "--ignore",
83
- "-i",
84
- is_eager=True,
85
- help="Serial port(s) to ignore. Defaults to MPFLASH_IGNORE.",
86
- callback=cb_ignore,
87
- multiple=True,
88
- default=[],
89
- envvar="MPFLASH_IGNORE",
90
- show_default=True,
91
- metavar="SERIALPORT",
92
- )
93
74
  @click.option(
94
75
  "--test",
95
76
  is_eager=True,
mpflash/cli_list.py CHANGED
@@ -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,21 +20,55 @@ 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) -> int:
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
mpflash/cli_main.py CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  # import rich_click as click
4
4
 
5
+ import os
6
+
5
7
  import click
6
8
  from loguru import logger as log
7
9
 
@@ -12,19 +14,24 @@ from .cli_list import cli_list_mcus
12
14
 
13
15
 
14
16
  def mpflash():
15
- cli.add_command(cli_flash_board)
16
17
  cli.add_command(cli_list_mcus)
17
18
  cli.add_command(cli_download)
19
+ cli.add_command(cli_flash_board)
20
+
18
21
  # cli(auto_envvar_prefix="MPFLASH")
19
- try:
22
+ if False and os.environ.get("COMPUTERNAME") == "JOSVERL-S4":
23
+ # intentional less error suppression on dev machine
20
24
  result = cli(standalone_mode=False)
21
- exit(result)
22
- except AttributeError as e:
23
- log.error(f"Error: {e}")
24
- exit(-1)
25
- except click.exceptions.ClickException as e:
26
- log.error(f"Error: {e}")
27
- exit(-2)
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)
28
35
 
29
36
 
30
37
  if __name__ == "__main__":
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/connected.py ADDED
@@ -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
mpflash/download.py CHANGED
@@ -18,10 +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
22
  from mpflash.errors import MPFlashError
23
23
  from mpflash.mpboard_id import get_known_ports
24
24
 
25
+ # avoid conflict with the ujson used by MicroPython
25
26
  jsonlines.ujson = None # type: ignore
26
27
  # #########################################################################################################
27
28
 
@@ -31,8 +32,18 @@ MICROPYTHON_ORG_URL = "https://micropython.org/"
31
32
  # Regexes to remove dates and hashes in the filename that just get in the way
32
33
  RE_DATE = r"(-\d{8}-)"
33
34
  RE_HASH = r"(.g[0-9a-f]+\.)"
34
- # regex to extract the version from the firmware filename
35
- RE_VERSION_PREVIEW = r"(\d+\.\d+(\.\d+)?(-\w+.\d+)?)"
35
+ # regex to extract the version and the build from the firmware filename
36
+ # group 1 is the version, group 2 is the build
37
+ RE_VERSION_PREVIEW = r"v([\d\.]+)-?(?:preview\.)?(\d+)?\."
38
+ # 'RPI_PICO_W-v1.23.uf2'
39
+ # 'RPI_PICO_W-v1.23.0.uf2'
40
+ # 'RPI_PICO_W-v1.23.0-406.uf2'
41
+ # 'RPI_PICO_W-v1.23.0-preview.406.uf2'
42
+ # 'RPI_PICO_W-v1.23.0-preview.4.uf2'
43
+ # 'RPI_PICO_W-v1.23.0.uf2'
44
+ # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.gc1a6b95bf.uf2'
45
+ # 'https://micropython.org/resources/firmware/RPI_PICO_W-20240531-v1.24.0-preview.10.uf2'
46
+ # 'RPI_PICO_W-v1.24.0-preview.10.gc1a6b95bf.uf2'
36
47
 
37
48
 
38
49
  # use functools.lru_cache to avoid needing to download pages multiple times
@@ -51,6 +62,10 @@ def get_board_urls(page_url: str) -> List[Dict[str, str]]:
51
62
 
52
63
  Args:
53
64
  page_url (str): The url of the page to get the board urls from.
65
+
66
+ Returns:
67
+ List[Dict[str, str]]: A list of dictionaries containing the board name and url.
68
+
54
69
  """
55
70
  downloads_html = get_page(page_url)
56
71
  soup = BeautifulSoup(downloads_html, "html.parser")
@@ -89,14 +104,11 @@ def board_firmware_urls(board_url: str, base_url: str, ext: str) -> List[str]:
89
104
  return links
90
105
 
91
106
 
92
- # type alias for the firmware info
93
- FirmwareInfo = Dict[str, str]
94
-
95
-
96
107
  # boards we are interested in ( this avoids getting a lot of boards we don't care about)
97
108
  # The first run takes ~60 seconds to run for 4 ports , all boards
98
109
  # so it makes sense to cache the results and skip boards as soon as possible
99
- def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FirmwareInfo]:
110
+ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[FWInfo]:
111
+ # sourcery skip: use-getitem-for-re-match-groups
100
112
  """
101
113
  Retrieves a list of firmware information for the specified ports and boards.
102
114
 
@@ -106,59 +118,70 @@ def get_boards(ports: List[str], boards: List[str], clean: bool) -> List[Firmwar
106
118
  clean (bool): A flag indicating whether to perform a clean retrieval.
107
119
 
108
120
  Returns:
109
- List[FirmwareInfo]: A list of firmware information for the specified ports and boards.
121
+ List[FWInfo]: A list of firmware information for the specified ports and boards.
110
122
 
111
123
  """
112
- board_urls: List[FirmwareInfo] = []
124
+ board_urls: List[FWInfo] = []
113
125
  if ports is None:
114
126
  ports = get_known_ports()
115
127
  for port in ports:
116
128
  download_page_url = f"{MICROPYTHON_ORG_URL}download/?port={port}"
117
- _urls = get_board_urls(download_page_url)
129
+ urls = get_board_urls(download_page_url)
118
130
  # filter out boards we don't care about
119
- _urls = [board for board in _urls if board["board"] in boards]
131
+ urls = [board for board in urls if board["board"] in boards]
120
132
  # add the port to the board urls
121
- for board in _urls:
133
+ for board in urls:
122
134
  board["port"] = port
123
135
 
124
- for board in track(_urls, description=f"Checking {port} download pages", transient=True,refresh_per_second=2):
136
+ for board in track(urls, description=f"Checking {port} download pages", transient=True, refresh_per_second=2):
125
137
  # add a board to the list for each firmware found
126
- firmwares = []
138
+ firmware_urls: List[str] = []
127
139
  for ext in PORT_FWTYPES[port]:
128
- firmwares += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
129
-
130
- for _url in firmwares:
140
+ firmware_urls += board_firmware_urls(board["url"], MICROPYTHON_ORG_URL, ext)
141
+ for _url in firmware_urls:
131
142
  board["firmware"] = _url
132
- board["preview"] = "preview" in _url # type: ignore
133
- if ver_match := re.search(RE_VERSION_PREVIEW, _url):
134
- board["version"] = ver_match[1]
135
- else:
136
- board["version"] = ""
137
- if "preview." in board["version"]:
138
- board["build"] = board["version"].split("preview.")[-1]
139
- else:
140
- board["build"] = "0"
141
143
  fname = Path(board["firmware"]).name
142
144
  if clean:
143
145
  # remove date from firmware name
144
146
  fname = re.sub(RE_DATE, "-", fname)
145
147
  # remove hash from firmware name
146
148
  fname = re.sub(RE_HASH, ".", fname)
147
- board["filename"] = fname
148
- board["ext"] = Path(board["firmware"]).suffix
149
- board["variant"] = board["filename"].split("-v")[0] if "-v" in board["filename"] else ""
150
- board_urls.append(board.copy())
149
+ fw_info = FWInfo(
150
+ filename=fname,
151
+ port=port,
152
+ board=board["board"],
153
+ preview="preview" in _url,
154
+ firmware=_url,
155
+ version="",
156
+ )
157
+ # board["firmware"] = _url
158
+ # board["preview"] = "preview" in _url # type: ignore
159
+ if ver_match := re.search(RE_VERSION_PREVIEW, _url):
160
+ fw_info.version = ver_match.group(1)
161
+ fw_info.build = ver_match.group(2) or "0"
162
+ fw_info.preview = fw_info.build != "0"
163
+ # # else:
164
+ # # board.$1= ""
165
+ # if "preview." in fw_info.version:
166
+ # fw_info.build = fw_info.version.split("preview.")[-1]
167
+ # else:
168
+ # fw_info.build = "0"
169
+
170
+ fw_info.ext = Path(fw_info.firmware).suffix
171
+ fw_info.variant = fw_info.filename.split("-v")[0] if "-v" in fw_info.filename else ""
172
+
173
+ board_urls.append(fw_info)
151
174
  return board_urls
152
175
 
153
176
 
154
- def key_fw_ver_pre_ext_bld(x: FirmwareInfo):
177
+ def key_fw_ver_pre_ext_bld(x: FWInfo):
155
178
  "sorting key for the retrieved board urls"
156
- return x["variant"], x["version"], x["preview"], x["ext"], x["build"]
179
+ return x.variant, x.version, x.preview, x.ext, x.build
157
180
 
158
181
 
159
- def key_fw_var_pre_ext(x: FirmwareInfo):
182
+ def key_fw_var_pre_ext(x: FWInfo):
160
183
  "Grouping key for the retrieved board urls"
161
- return x["variant"], x["preview"], x["ext"]
184
+ return x.variant, x.preview, x.ext
162
185
 
163
186
 
164
187
  def download_firmwares(
@@ -177,32 +200,37 @@ def download_firmwares(
177
200
  unique_boards = get_firmware_list(ports, boards, versions, clean)
178
201
 
179
202
  for b in unique_boards:
180
- log.debug(b["filename"])
203
+ log.debug(b.filename)
181
204
  # relevant
182
205
 
183
206
  log.info(f"Found {len(unique_boards)} relevant unique firmwares")
207
+ if not unique_boards:
208
+ log.error("No relevant firmwares could be found on https://micropython.org/download")
209
+ log.info(f"{versions=} {ports=} {boards=}")
210
+ log.info("Please check the website for the latest firmware files or try the preview version.")
211
+ return 0
184
212
 
185
213
  firmware_folder.mkdir(exist_ok=True)
186
214
 
187
215
  with jsonlines.open(firmware_folder / "firmware.jsonl", "a") as writer:
188
216
  for board in unique_boards:
189
- filename = firmware_folder / board["port"] / board["filename"]
217
+ filename = firmware_folder / board.port / board.filename
190
218
  filename.parent.mkdir(exist_ok=True)
191
219
  if filename.exists() and not force:
192
220
  skipped += 1
193
221
  log.debug(f" {filename} already exists, skip download")
194
222
  continue
195
- log.info(f"Downloading {board['firmware']}")
223
+ log.info(f"Downloading {board.firmware}")
196
224
  log.info(f" to {filename}")
197
225
  try:
198
- r = requests.get(board["firmware"], allow_redirects=True)
226
+ r = requests.get(board.firmware, allow_redirects=True)
199
227
  with open(filename, "wb") as fw:
200
228
  fw.write(r.content)
201
- board["filename"] = str(filename.relative_to(firmware_folder))
229
+ board.filename = str(filename.relative_to(firmware_folder))
202
230
  except requests.RequestException as e:
203
231
  log.exception(e)
204
232
  continue
205
- writer.write(board)
233
+ writer.write(board.to_dict())
206
234
  downloaded += 1
207
235
  log.info(f"Downloaded {downloaded} firmwares, skipped {skipped} existing files.")
208
236
  return downloaded + skipped
@@ -219,7 +247,7 @@ def get_firmware_list(ports: List[str], boards: List[str], versions: List[str],
219
247
  clean : A flag indicating whether to perform a clean check.
220
248
 
221
249
  Returns:
222
- List[FirmwareInfo]: A list of unique firmware information.
250
+ List[FWInfo]: A list of unique firmware information.
223
251
 
224
252
  """
225
253
 
@@ -228,15 +256,18 @@ def get_firmware_list(ports: List[str], boards: List[str], versions: List[str],
228
256
  board_urls = sorted(get_boards(ports, boards, clean), key=key_fw_ver_pre_ext_bld)
229
257
 
230
258
  log.debug(f"Total {len(board_urls)} firmwares")
259
+
231
260
  relevant = [
232
261
  board
233
262
  for board in board_urls
234
- if board["board"] in boards and (board["version"] in versions or board["preview"] and preview)
235
- # and b["port"] in ["esp32", "rp2"]
263
+ if board.version in versions and board.build == "0" and board.board in boards and not board.preview
236
264
  ]
265
+
266
+ if preview:
267
+ relevant.extend([board for board in board_urls if board.board in boards and board.preview])
237
268
  log.debug(f"Matching firmwares: {len(relevant)}")
238
269
  # select the unique boards
239
- unique_boards: List[FirmwareInfo] = []
270
+ unique_boards: List[FWInfo] = []
240
271
  for _, g in itertools.groupby(relevant, key=key_fw_var_pre_ext):
241
272
  # list is aleady sorted by build so we can just get the last item
242
273
  sub_list = list(g)
@@ -280,5 +311,10 @@ def download(
280
311
  except (PermissionError, FileNotFoundError) as e:
281
312
  log.critical(f"Could not create folder {destination}")
282
313
  raise MPFlashError(f"Could not create folder {destination}") from e
314
+ try:
315
+ result = download_firmwares(destination, ports, boards, versions, force=force, clean=clean)
316
+ except requests.exceptions.RequestException as e:
317
+ log.exception(e)
318
+ raise MPFlashError("Could not connect to micropython.org") from e
283
319
 
284
- return download_firmwares(destination, ports, boards, versions, force=force, clean=clean)
320
+ return result