mpflash 1.25.0rc4__py3-none-any.whl → 1.25.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.
mpflash/basicgit.py CHANGED
@@ -135,6 +135,25 @@ def get_local_tags(repo: Optional[Path] = None, minver: Optional[str] = None) ->
135
135
  return sorted(tags)
136
136
 
137
137
 
138
+ def get_current_branch(repo: Optional[Union[Path, str]] = None) -> Optional[str]:
139
+ """
140
+ Get the current branch name of a local repository.
141
+
142
+ Args:
143
+ repo: Path to the repository directory
144
+
145
+ Returns:
146
+ Current branch name or None if error
147
+ """
148
+ cmd = ["git", "branch", "--show-current"]
149
+ result = _run_local_git(cmd, repo=repo, expect_stderr=True)
150
+ if not result:
151
+ return None
152
+
153
+ branch = result.stdout.strip()
154
+ return branch if branch else None
155
+
156
+
138
157
  @cachetools.func.ttl_cache(maxsize=16, ttl=60) # 60 seconds
139
158
  def get_tags(repo: str, minver: Optional[str] = None) -> List[str]:
140
159
  """
mpflash/cli_add.py ADDED
@@ -0,0 +1,131 @@
1
+ """CLI to add a custom MicroPython firmware."""
2
+
3
+ from pathlib import Path
4
+ from typing import Union
5
+
6
+ import rich_click as click
7
+ from loguru import logger as log
8
+
9
+ from mpflash.connected import connected_ports_boards_variants
10
+ from mpflash.custom import Firmware, add_firmware, custom_fw_from_path
11
+ from mpflash.downloaded import clean_downloaded_firmwares
12
+ from mpflash.errors import MPFlashError
13
+ from mpflash.mpboard_id import find_known_board
14
+ from mpflash.mpboard_id.alternate import add_renamed_boards
15
+ from mpflash.versions import clean_version
16
+
17
+ from .ask_input import ask_missing_params
18
+ from .cli_group import cli
19
+ from .config import config
20
+ from .download import download
21
+
22
+
23
+ @cli.command(
24
+ "add",
25
+ help="Add a custom MicroPython firmware.",
26
+ )
27
+ # @click.option(
28
+ # "--version",
29
+ # "-v",
30
+ # "versions",
31
+ # default=["stable"],
32
+ # multiple=False,
33
+ # show_default=True,
34
+ # help="The version of MicroPython to to download.",
35
+ # metavar="SEMVER, 'stable', 'preview' or '?'",
36
+ # )
37
+ @click.option(
38
+ "--path",
39
+ "-p",
40
+ "fw_path",
41
+ multiple=False,
42
+ default="",
43
+ show_default=False,
44
+ help="a local path to the firmware file to add.",
45
+ metavar="FIRMWARE_PATH",
46
+ )
47
+ @click.option(
48
+ "--description",
49
+ "-d",
50
+ "description",
51
+ default="",
52
+ help="An Optional description for the firmware.",
53
+ metavar="TXT",
54
+ )
55
+ # @click.option(
56
+ # "--board",
57
+ # "-b",
58
+ # "boards",
59
+ # multiple=True,
60
+ # default=[],
61
+ # show_default=True,
62
+ # help="The board(s) to download the firmware for.",
63
+ # metavar="BOARD_ID or ?",
64
+ # )
65
+ # @click.option(
66
+ # "--serial",
67
+ # "--serial-port",
68
+ # "-s",
69
+ # "serial",
70
+ # default=["*"],
71
+ # show_default=True,
72
+ # multiple=True,
73
+ # help="Which serial port(s) (or globs) to flash",
74
+ # metavar="SERIALPORT",
75
+ # )
76
+ # @click.option(
77
+ # "--ignore",
78
+ # "-i",
79
+ # is_eager=True,
80
+ # help="Serial port(s) to ignore. Defaults to MPFLASH_IGNORE.",
81
+ # multiple=True,
82
+ # default=[],
83
+ # envvar="MPFLASH_IGNORE",
84
+ # show_default=True,
85
+ # metavar="SERIALPORT",
86
+ # )
87
+ # @click.option(
88
+ # "--clean/--no-clean",
89
+ # default=True,
90
+ # show_default=True,
91
+ # help="""Remove dates and hashes from the downloaded firmware filenames.""",
92
+ # )
93
+ @click.option(
94
+ "--force",
95
+ "-f",
96
+ default=False,
97
+ is_flag=True,
98
+ show_default=True,
99
+ help="""Overwrite existing firmware.""",
100
+ )
101
+ def cli_add_custom(
102
+ fw_path: Union[Path, str],
103
+ force: bool = False,
104
+ description: str = "",
105
+ ) -> int:
106
+ """Add a custom MicroPython firmware from a local file."""
107
+ if not fw_path:
108
+ log.error("No firmware path provided. Use --path to specify a firmware file.")
109
+ return 1
110
+ fw_path = Path(fw_path).expanduser().resolve()
111
+ if not fw_path.exists():
112
+ log.error(f"Firmware file does not exist: {fw_path}")
113
+ return 1
114
+
115
+ try:
116
+ fw_dict = custom_fw_from_path(fw_path)
117
+ if description:
118
+ fw_dict["description"] = description
119
+ if add_firmware(
120
+ source=fw_path,
121
+ fw_info=fw_dict,
122
+ custom=True,
123
+ force=force,
124
+ ):
125
+ log.success(f"Added custom firmware: {fw_dict['custom_id']} for {fw_dict['firmware_file']}")
126
+ return 0
127
+ else:
128
+ return 1
129
+ except MPFlashError as e:
130
+ log.error(f"{e}")
131
+ return 1
mpflash/cli_download.py CHANGED
@@ -2,10 +2,11 @@
2
2
 
3
3
  from pathlib import Path
4
4
 
5
+ from pytest import param
5
6
  import rich_click as click
6
7
  from loguru import logger as log
7
8
 
8
- from mpflash.connected import connected_ports_boards
9
+ from mpflash.connected import connected_ports_boards_variants
9
10
  from mpflash.downloaded import clean_downloaded_firmwares
10
11
  from mpflash.errors import MPFlashError
11
12
  from mpflash.mpboard_id import find_known_board
@@ -23,15 +24,6 @@ from .download import download
23
24
  "download",
24
25
  help="Download MicroPython firmware for specific ports, boards and versions.",
25
26
  )
26
- @click.option(
27
- "--destination",
28
- "-d",
29
- "fw_folder",
30
- type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
31
- default=None,
32
- show_default=False,
33
- help="The folder to download the firmware to.",
34
- )
35
27
  @click.option(
36
28
  "--version",
37
29
  "-v",
@@ -94,9 +86,6 @@ def cli_download(**kwargs) -> int:
94
86
  params.boards = list(params.boards)
95
87
  params.serial = list(params.serial)
96
88
  params.ignore = list(params.ignore)
97
- if params.fw_folder:
98
- config.firmware_folder = Path(params.fw_folder)
99
- # all_boards: List[MPRemoteBoard] = []
100
89
  if params.boards:
101
90
  if not params.ports:
102
91
  # no ports specified - resolve ports from specified boards by resolving board IDs
@@ -109,7 +98,7 @@ def cli_download(**kwargs) -> int:
109
98
  log.error(f"{e}")
110
99
  else:
111
100
  # no boards specified - detect connected ports and boards
112
- params.ports, params.boards, _ = connected_ports_boards(include=params.serial, ignore=params.ignore)
101
+ params.ports, params.boards, _ , _ = connected_ports_boards_variants(include=params.serial, ignore=params.ignore)
113
102
 
114
103
  params = ask_missing_params(params)
115
104
  if not params: # Cancelled by user
mpflash/cli_flash.py CHANGED
@@ -1,4 +1,3 @@
1
- from pathlib import Path
2
1
  from typing import List
3
2
 
4
3
  import rich_click as click
@@ -7,11 +6,10 @@ from loguru import logger as log
7
6
  import mpflash.download.jid as jid
8
7
  import mpflash.mpboard_id as mpboard_id
9
8
  from mpflash.ask_input import ask_missing_params
10
- from mpflash.cli_download import connected_ports_boards
9
+ from mpflash.cli_download import connected_ports_boards_variants
11
10
  from mpflash.cli_group import cli
12
11
  from mpflash.cli_list import show_mcus
13
- from mpflash.common import BootloaderMethod, FlashParams, Params
14
- from mpflash.config import config
12
+ from mpflash.common import BootloaderMethod, FlashParams, filtered_comports
15
13
  from mpflash.errors import MPFlashError
16
14
  from mpflash.flash import flash_list
17
15
  from mpflash.flash.worklist import WorkList, full_auto_worklist, manual_worklist, single_auto_worklist
@@ -27,15 +25,6 @@ from mpflash.versions import clean_version
27
25
  "flash",
28
26
  short_help="Flash one or all connected MicroPython boards with a specific firmware and version.",
29
27
  )
30
- @click.option(
31
- "--firmware",
32
- "-f",
33
- "fw_folder",
34
- type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
35
- default=None,
36
- show_default=False,
37
- help="The folder to retrieve the firmware from.",
38
- )
39
28
  @click.option(
40
29
  "--version",
41
30
  "-v",
@@ -95,7 +84,7 @@ from mpflash.versions import clean_version
95
84
  )
96
85
  @click.option(
97
86
  "--variant",
98
- "-var",
87
+ "--var",
99
88
  "variant", # single board
100
89
  multiple=False,
101
90
  help="The board VARIANT to flash or '-'. If not specified will try to read the variant from the connected MCU.",
@@ -104,7 +93,6 @@ from mpflash.versions import clean_version
104
93
  @click.option(
105
94
  "--cpu",
106
95
  "--chip",
107
- "-c",
108
96
  "cpu",
109
97
  help="The CPU type to flash. If not specified will try to read the CPU from the connected MCU.",
110
98
  metavar="CPU",
@@ -134,12 +122,20 @@ from mpflash.versions import clean_version
134
122
  )
135
123
  @click.option(
136
124
  "--flash_mode",
137
- "-fm",
125
+ "--fm",
138
126
  type=click.Choice(["keep", "qio", "qout", "dio", "dout"]),
139
127
  default="keep",
140
128
  show_default=True,
141
129
  help="""Flash mode for ESP boards. (default: keep)""",
142
130
  )
131
+ @click.option(
132
+ "--custom",
133
+ "-c",
134
+ default=False,
135
+ is_flag=True,
136
+ show_default=True,
137
+ help="""Flash a custom firmware""",
138
+ )
143
139
  def cli_flash_board(**kwargs) -> int:
144
140
  # version to versions, board to boards
145
141
  kwargs["versions"] = [kwargs.pop("version")] if kwargs["version"] is not None else []
@@ -164,16 +160,18 @@ def cli_flash_board(**kwargs) -> int:
164
160
  # No bard specified
165
161
  params.boards = ["?"]
166
162
 
167
- if params.fw_folder:
168
- config.firmware_folder = Path(params.fw_folder)
169
163
  # Detect connected boards if not specified,
170
164
  # and ask for input if boards cannot be detected
171
165
  all_boards: List[MPRemoteBoard] = []
172
166
  if not params.boards:
173
167
  # nothing specified - detect connected boards
174
- params.ports, params.boards, all_boards = connected_ports_boards(
175
- include=params.ports, ignore=params.ignore, bluetooth=params.bluetooth
168
+ params.ports, params.boards, variants, all_boards = connected_ports_boards_variants(
169
+ include=params.ports,
170
+ ignore=params.ignore,
171
+ bluetooth=params.bluetooth,
176
172
  )
173
+ if variants and len(variants) >= 1:
174
+ params.variant = variants[0]
177
175
  if params.boards == []:
178
176
  # No MicroPython boards detected, but it could be unflashed or in bootloader mode
179
177
  # Ask for serial port and board_id to flash
@@ -197,11 +195,27 @@ def cli_flash_board(**kwargs) -> int:
197
195
  params.versions = [clean_version(v) for v in params.versions]
198
196
  worklist: WorkList = []
199
197
 
198
+ if len(params.versions) == 1 and len(params.boards) == 1 and params.serial == ["*"]:
199
+ # A one or more serial port including the board / variant
200
+ comports = filtered_comports(
201
+ ignore=params.ignore,
202
+ include=params.serial,
203
+ bluetooth=params.bluetooth,
204
+ )
205
+ board_id = f"{params.boards[0]}-{params.variant}" if params.variant else params.boards[0]
206
+ log.info(f"Flashing {board_id} {params.versions[0]} to {len(comports)} serial ports")
207
+ log.info(f"Target ports: {', '.join(comports)}")
208
+ worklist = manual_worklist(
209
+ comports,
210
+ board_id=board_id,
211
+ version=params.versions[0],
212
+ custom=params.custom,
213
+ )
200
214
  # if serial port == auto and there are one or more specified/detected boards
201
- if params.serial == ["*"] and params.boards:
215
+ elif params.serial == ["*"] and params.boards:
202
216
  if not all_boards:
203
217
  log.trace("No boards detected yet, scanning for connected boards")
204
- _, _, all_boards = connected_ports_boards(include=params.ports, ignore=params.ignore)
218
+ _, _, _, all_boards = connected_ports_boards_variants(include=params.ports, ignore=params.ignore)
205
219
  # if variant id provided on the cmdline, treat is as an override
206
220
  if params.variant:
207
221
  for b in all_boards:
@@ -215,8 +229,13 @@ def cli_flash_board(**kwargs) -> int:
215
229
  )
216
230
  elif params.versions[0] and params.boards[0] and params.serial:
217
231
  # A one or more serial port including the board / variant
232
+ comports = filtered_comports(
233
+ ignore=params.ignore,
234
+ include=params.ports,
235
+ bluetooth=params.bluetooth,
236
+ )
218
237
  worklist = manual_worklist(
219
- params.serial[0],
238
+ comports,
220
239
  board_id=params.boards[0],
221
240
  version=params.versions[0],
222
241
  )
@@ -226,7 +245,8 @@ def cli_flash_board(**kwargs) -> int:
226
245
  serial=params.serial[0],
227
246
  version=params.versions[0],
228
247
  )
229
- jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
248
+ if not params.custom:
249
+ jid.ensure_firmware_downloaded(worklist, version=params.versions[0], force=params.force)
230
250
  if flashed := flash_list(
231
251
  worklist,
232
252
  params.erase,
@@ -239,5 +259,3 @@ def cli_flash_board(**kwargs) -> int:
239
259
  else:
240
260
  log.error("No boards were flashed")
241
261
  return 1
242
-
243
-
mpflash/cli_group.py CHANGED
@@ -14,6 +14,7 @@ from .logger import log, make_quiet, set_loglevel
14
14
  set_loglevel("INFO")
15
15
  config.verbose = False
16
16
 
17
+
17
18
  def cb_verbose(ctx, param, value):
18
19
  """Callback to set the log level to DEBUG if verbose is set"""
19
20
  if value and not config.quiet:
mpflash/cli_list.py CHANGED
@@ -86,6 +86,8 @@ def cli_list_mcus(serial: List[str], ignore: List[str], bluetooth: bool, as_json
86
86
  if mcu.family == "circuitpython":
87
87
  # CircuitPython boards need a special reset command
88
88
  mcu.run_command(["exec", "--no-follow", "import microcontroller,time;time.sleep(0.01);microcontroller.reset()"], resume=False)
89
+ elif mcu.family == "unknown":
90
+ continue
89
91
  else:
90
92
  mcu.run_command("reset")
91
93
  return 0 if conn_mcus else 1
mpflash/cli_main.py CHANGED
@@ -5,6 +5,9 @@ import os
5
5
  import click.exceptions as click_exceptions
6
6
  from loguru import logger as log
7
7
 
8
+ from mpflash.errors import MPFlashError
9
+
10
+ from .cli_add import cli_add_custom
8
11
  from .cli_download import cli_download
9
12
  from .cli_flash import cli_flash_board
10
13
  from .cli_group import cli
@@ -19,6 +22,7 @@ def mpflash():
19
22
  cli.add_command(cli_list_mcus)
20
23
  cli.add_command(cli_download)
21
24
  cli.add_command(cli_flash_board)
25
+ cli.add_command(cli_add_custom)
22
26
 
23
27
  # cli(auto_envvar_prefix="MPFLASH")
24
28
  if False and os.environ.get("COMPUTERNAME").upper().startswith("JOSVERL"):
@@ -37,6 +41,9 @@ def mpflash():
37
41
  except click_exceptions.Abort:
38
42
  # Aborted - Ctrl-C
39
43
  exit(-3)
44
+ except MPFlashError as e:
45
+ log.error(f"MPFlashError: {e}")
46
+ exit(-4)
40
47
 
41
48
 
42
49
  if __name__ == "__main__":
mpflash/common.py CHANGED
@@ -4,7 +4,6 @@ import os
4
4
  import platform
5
5
  from dataclasses import dataclass, field
6
6
  from enum import Enum
7
- from pathlib import Path
8
7
  from typing import List, Optional, Union
9
8
 
10
9
  from serial.tools import list_ports
@@ -36,7 +35,6 @@ class Params:
36
35
  boards: List[str] = field(default_factory=list)
37
36
  variant: str = ""
38
37
  versions: List[str] = field(default_factory=list)
39
- fw_folder: Optional[Path] = None
40
38
  serial: List[str] = field(default_factory=list)
41
39
  ignore: List[str] = field(default_factory=list)
42
40
  bluetooth: bool = False
@@ -66,6 +64,7 @@ class FlashParams(Params):
66
64
  bootloader: BootloaderMethod = BootloaderMethod.NONE
67
65
  cpu: str = ""
68
66
  flash_mode: str = "keep" # keep, qio, qout, dio, dout
67
+ custom: bool = False
69
68
 
70
69
  def __post_init__(self):
71
70
  if isinstance(self.bootloader, str):
@@ -79,11 +78,24 @@ def filtered_comports(
79
78
  ignore: Optional[List[str]] = None,
80
79
  include: Optional[List[str]] = None,
81
80
  bluetooth: bool = False,
81
+ ) -> List[str]:
82
+ """
83
+ Get a list of filtered comports using the include and ignore lists.
84
+ both can be globs (e.g. COM*) or exact port names (e.g. COM1)
85
+ """
86
+ return [p.device for p in filtered_portinfos(ignore, include, bluetooth)]
87
+
88
+
89
+ def filtered_portinfos(
90
+ ignore: Optional[List[str]] = None,
91
+ include: Optional[List[str]] = None,
92
+ bluetooth: bool = False,
82
93
  ) -> List[ListPortInfo]: # sourcery skip: assign-if-exp
83
94
  """
84
95
  Get a list of filtered comports using the include and ignore lists.
85
96
  both can be globs (e.g. COM*) or exact port names (e.g. COM1)
86
97
  """
98
+ log.trace(f"filtered_portinfos: {ignore=}, {include=}, {bluetooth=}")
87
99
  if not ignore:
88
100
  ignore = []
89
101
  elif not isinstance(ignore, list): # type: ignore
mpflash/config.py CHANGED
@@ -7,6 +7,8 @@ from typing import List, Optional
7
7
 
8
8
  import platformdirs
9
9
 
10
+ from mpflash.errors import MPFlashError
11
+
10
12
 
11
13
  def get_version():
12
14
  name = __package__ or "mpflash"
@@ -44,7 +46,18 @@ class MPFlashConfig:
44
46
  def firmware_folder(self) -> Path:
45
47
  """The folder where firmware files are stored"""
46
48
  if not self._firmware_folder:
47
- self._firmware_folder = platformdirs.user_downloads_path() / "firmware"
49
+ from mpflash.logger import log
50
+
51
+ # Check if MPFLASH_FIRMWARE environment variable is set
52
+ env_firmware_path = os.getenv("MPFLASH_FIRMWARE")
53
+ if env_firmware_path:
54
+ firmware_path = Path(env_firmware_path).expanduser().resolve()
55
+ if firmware_path.exists() and firmware_path.is_dir():
56
+ self._firmware_folder = firmware_path
57
+ else:
58
+ log.warning(
59
+ f"Environment variable MPFLASH_FIRMWARE points to invalid directory: {env_firmware_path}. Using default location."
60
+ )
48
61
  # allow testing in CI
49
62
  if Path(os.getenv("GITHUB_ACTIONS", "")).as_posix().lower() == "true":
50
63
  workspace = os.getenv("GITHUB_WORKSPACE")
@@ -53,6 +66,13 @@ class MPFlashConfig:
53
66
  ws_path.mkdir(parents=True, exist_ok=True)
54
67
  print(f"Detected GitHub Actions environment. Using workspace path: {ws_path}")
55
68
  self._firmware_folder = ws_path
69
+ if not self._firmware_folder:
70
+ self._firmware_folder = platformdirs.user_downloads_path() / "firmware"
71
+ if not self._firmware_folder.exists():
72
+ log.info(f"Creating firmware folder at {self._firmware_folder}")
73
+ self._firmware_folder.mkdir(parents=True, exist_ok=True)
74
+ if not self._firmware_folder.is_dir():
75
+ raise MPFlashError(f"Firmware folder {self._firmware_folder} is not a directory.")
56
76
  return self._firmware_folder
57
77
 
58
78
  @firmware_folder.setter
@@ -67,6 +87,7 @@ class MPFlashConfig:
67
87
  def db_path(self) -> Path:
68
88
  """The path to the database file"""
69
89
  return self.firmware_folder / "mpflash.db"
90
+
70
91
  @property
71
92
  def db_version(self) -> str:
72
93
  return "1.24.1"
mpflash/connected.py CHANGED
@@ -4,13 +4,13 @@ from rich import print
4
4
  from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
5
5
  from rich.table import Column
6
6
 
7
- from mpflash.common import filtered_comports, find_serial_by_path
7
+ from mpflash.common import filtered_portinfos, find_serial_by_path
8
8
  from mpflash.mpremoteboard import MPRemoteBoard
9
9
 
10
10
 
11
- def connected_ports_boards(
11
+ def connected_ports_boards_variants(
12
12
  *, include: List[str], ignore: List[str], bluetooth: bool = False
13
- ) -> Tuple[List[str], List[str], List[MPRemoteBoard]]:
13
+ ) -> Tuple[List[str], List[str], List[str], List[MPRemoteBoard]]:
14
14
  """
15
15
  Returns a tuple containing lists of unique ports and boards from the connected MCUs.
16
16
  Boards that are physically connected, but give no tangible response are ignored.
@@ -21,13 +21,15 @@ def connected_ports_boards(
21
21
  - A list of unique board names of the connected MCUs.
22
22
  - A list of MPRemoteBoard instances of the connected MCUs.
23
23
  """
24
+ # conn_mcus = [b for b in list_mcus(include=include, ignore=ignore, bluetooth=bluetooth)]
24
25
  conn_mcus = [b for b in list_mcus(include=include, ignore=ignore, bluetooth=bluetooth) if b.connected]
25
26
  # ignore boards that have the [mpflash] ignore flag set
26
27
  conn_mcus = [item for item in conn_mcus if not (item.toml.get("mpflash", {}).get("ignore", False))]
27
28
 
28
29
  ports = list({b.port for b in conn_mcus})
29
30
  boards = list({b.board for b in conn_mcus})
30
- return (ports, boards, conn_mcus)
31
+ variants = list({b.variant for b in conn_mcus if b.variant})
32
+ return (ports, boards, variants, conn_mcus)
31
33
 
32
34
 
33
35
  # #########################################################################################################
@@ -46,8 +48,8 @@ def list_mcus(*, ignore: List[str], include: List[str], bluetooth: bool = False)
46
48
  ConnectionError: If there is an error connecting to a board.
47
49
  """
48
50
  # conn_mcus = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards(bluetooth) if sp not in config.ignore_ports]
49
-
50
- comports = filtered_comports(
51
+ vid_pid = True
52
+ comports = filtered_portinfos(
51
53
  ignore=ignore,
52
54
  include=include,
53
55
  bluetooth=bluetooth,