mpflash 0.4.0.post3__py3-none-any.whl → 0.5.0__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/ask_input.py ADDED
@@ -0,0 +1,163 @@
1
+ """Download input handling for mpflash."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import List, Sequence, Tuple, Union
6
+
7
+ from loguru import logger as log
8
+
9
+ from mpflash.common import micropython_versions
10
+ from mpflash.config import config
11
+ from mpflash.mpboard_id.api import known_mp_boards, known_mp_ports
12
+ from mpflash.mpremoteboard import MPRemoteBoard
13
+
14
+
15
+ @dataclass
16
+ class Params:
17
+ ports: List[str] = field(default_factory=list)
18
+ boards: List[str] = field(default_factory=list)
19
+ versions: List[str] = field(default_factory=list)
20
+ fw_folder: Path = Path()
21
+
22
+
23
+ @dataclass
24
+ class DownloadParams(Params):
25
+ clean: bool = False
26
+ force: bool = False
27
+
28
+
29
+ @dataclass
30
+ class FlashParams(Params):
31
+ # TODO: Should Serial port be a list?
32
+ serial: str = ""
33
+ erase: bool = True
34
+ bootloader: bool = True
35
+ cpu: str = ""
36
+
37
+
38
+ ParamType = Union[DownloadParams, FlashParams]
39
+
40
+
41
+ def ask_missing_params(
42
+ params: ParamType,
43
+ action: str = "download",
44
+ ) -> ParamType:
45
+ if not config.interactive:
46
+ # no interactivity allowed
47
+ return params
48
+ # import only when needed to reduce load time
49
+ import inquirer
50
+
51
+ questions = []
52
+ if isinstance(params, FlashParams) and (not params.serial or "?" in params.versions):
53
+ ask_serialport(questions, action=action)
54
+
55
+ if not params.versions or "?" in params.versions:
56
+ ask_versions(questions, action=action)
57
+
58
+ if not params.boards or "?" in params.boards:
59
+ ask_port_board(questions, action=action)
60
+
61
+ answers = inquirer.prompt(questions)
62
+ if not answers:
63
+ return params
64
+ # print(repr(answers))
65
+ if isinstance(params, FlashParams) and "serial" in answers:
66
+ params.serial = answers["serial"]
67
+ if "port" in answers:
68
+ params.ports = [answers["port"]]
69
+ if "boards" in answers:
70
+ params.boards = answers["boards"]
71
+ if "versions" in answers:
72
+ # make sure it is a list
73
+ params.versions = answers["versions"] if isinstance(answers["versions"], list) else [answers["versions"]]
74
+
75
+ log.debug(repr(params))
76
+
77
+ return params
78
+
79
+
80
+ def some_boards(answers: dict) -> Sequence[Tuple[str, str]]:
81
+ if "versions" in answers:
82
+ _versions = list(answers["versions"])
83
+ if "stable" in _versions:
84
+ _versions.remove("stable")
85
+ _versions.append(micropython_versions()[-2])
86
+ if "preview" in _versions:
87
+ _versions.remove("preview")
88
+ _versions.append(micropython_versions()[-1])
89
+ _versions.append(micropython_versions()[-2])
90
+
91
+ some_boards = known_mp_boards(answers["port"], _versions) # or known_mp_boards(answers["port"])
92
+ else:
93
+ some_boards = known_mp_boards(answers["port"])
94
+
95
+ if some_boards:
96
+ # Create a dictionary where the keys are the second elements of the tuples
97
+ # This will automatically remove duplicates because dictionaries cannot have duplicate keys
98
+ unique_dict = {item[1]: item for item in some_boards}
99
+ # Get the values of the dictionary, which are the unique items from the original list
100
+ some_boards = list(unique_dict.values())
101
+ else:
102
+ some_boards = [("No boards found", "")]
103
+ return some_boards
104
+
105
+
106
+ def ask_port_board(questions: list, *, action: str):
107
+ # import only when needed to reduce load time
108
+ import inquirer
109
+
110
+ questions.extend(
111
+ (
112
+ inquirer.List(
113
+ "port",
114
+ message=f"What port do you want to {action}?",
115
+ choices=known_mp_ports(),
116
+ autocomplete=True,
117
+ ),
118
+ inquirer.Checkbox(
119
+ "boards",
120
+ message=f"What board do you want to {action}?",
121
+ choices=some_boards,
122
+ validate=lambda _, x: True if x else "Please select at least one board", # type: ignore
123
+ ),
124
+ )
125
+ )
126
+
127
+
128
+ def ask_versions(questions: list, *, action: str):
129
+ # import only when needed to reduce load time
130
+ import inquirer
131
+
132
+ input_ux = inquirer.Checkbox if action == "download" else inquirer.List
133
+ mp_versions: List[str] = micropython_versions()
134
+ mp_versions = [v for v in mp_versions if "preview" not in v]
135
+ mp_versions.append("preview")
136
+ mp_versions.reverse() # newest first
137
+ questions.append(
138
+ input_ux(
139
+ "versions",
140
+ message=f"What version(s) do you want to {action}?",
141
+ choices=mp_versions,
142
+ autocomplete=True,
143
+ validate=lambda _, x: True if x else "Please select at least one version", # type: ignore
144
+ )
145
+ )
146
+
147
+
148
+ def ask_serialport(questions: list, *, action: str):
149
+ # import only when needed to reduce load time
150
+ import inquirer
151
+
152
+ serialports = MPRemoteBoard.connected_boards()
153
+ questions.append(
154
+ inquirer.List(
155
+ "serial",
156
+ message="What serial port do you want use ?",
157
+ validate=lambda _, x: True if x else "Please enter a serial port", # type: ignore
158
+ choices=serialports,
159
+ other=True,
160
+ )
161
+ )
162
+
163
+ return questions
mpflash/cli_download.py CHANGED
@@ -3,14 +3,15 @@
3
3
  from pathlib import Path
4
4
  from typing import List, Tuple
5
5
 
6
- from mpflash.common import clean_version
7
6
  import rich_click as click
8
7
 
8
+ from mpflash.common import clean_version
9
+
10
+ from .ask_input import DownloadParams, ask_missing_params
9
11
  from .cli_group import cli
12
+ from .cli_list import list_mcus
10
13
  from .config import config
11
14
  from .download import download
12
- from .download_input import DownloadParams, ask_missing_params
13
- from .cli_list import list_mcus
14
15
 
15
16
 
16
17
  def connected_ports_boards() -> Tuple[List[str], List[str]]:
@@ -27,6 +28,7 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
27
28
  @click.option(
28
29
  "--destination",
29
30
  "-d",
31
+ "fw_folder",
30
32
  type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
31
33
  default=config.firmware_folder,
32
34
  show_default=True,
@@ -36,18 +38,21 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
36
38
  "--version",
37
39
  "-v",
38
40
  "versions",
41
+ default=["stable"],
39
42
  multiple=True,
40
- help="The version of MicroPython to to download. Use 'preview' to include preview versions.",
41
43
  show_default=True,
42
- default=["stable"],
44
+ help="The version of MicroPython to to download.",
45
+ metavar="SEMVER, 'stable', 'preview' or '?'",
43
46
  )
44
47
  @click.option(
45
48
  "--board",
46
49
  "-b",
47
50
  "boards",
48
51
  multiple=True,
52
+ default=["?"],
49
53
  show_default=True,
50
- help="The board(s) to download the firmware for.", # Use '--board all' to download all boards.",
54
+ help="The board(s) to download the firmware for.",
55
+ metavar="BOARD_ID or ?",
51
56
  )
52
57
  @click.option(
53
58
  "--clean/--no-clean",
@@ -59,8 +64,8 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
59
64
  "--force",
60
65
  default=False,
61
66
  is_flag=True,
62
- help="""Force download of firmware even if it already exists.""",
63
67
  show_default=True,
68
+ help="""Force download of firmware even if it already exists.""",
64
69
  )
65
70
  def cli_download(
66
71
  **kwargs,
@@ -70,18 +75,17 @@ def cli_download(
70
75
  if not params.boards:
71
76
  # nothing specified - detect connected boards
72
77
  params.ports, params.boards = connected_ports_boards()
78
+ # ask for any remaining parameters
79
+ params = ask_missing_params(params, action="download")
80
+ assert isinstance(params, DownloadParams)
73
81
 
74
82
  params.versions = [clean_version(v, drop_v=True) for v in params.versions] # remove leading v from version
75
83
 
76
- # ask for any remaining parameters
77
- params = ask_missing_params(params)
78
- # preview is not a version, it is an option to include preview versions
79
84
  download(
80
- params.destination,
85
+ params.fw_folder,
81
86
  params.ports,
82
87
  params.boards,
83
88
  params.versions,
84
89
  params.force,
85
90
  params.clean,
86
- params.preview,
87
91
  )
mpflash/cli_flash.py CHANGED
@@ -1,21 +1,22 @@
1
-
2
1
  from pathlib import Path
3
- from typing import Optional
4
-
2
+ from typing import List
5
3
 
6
4
  import rich_click as click
7
5
  from loguru import logger as log
8
6
 
7
+ from .ask_input import FlashParams, ask_missing_params
8
+ from .cli_download import connected_ports_boards
9
9
  from .cli_group import cli
10
-
11
- from .flash_esp import flash_esp
12
- from .flash_stm32 import flash_stm32
13
- from .flash_uf2 import flash_uf2
14
10
  from .cli_list import show_mcus
11
+ from .common import clean_version
15
12
  from .config import config
16
13
  from .flash import WorkList, auto_update, enter_bootloader, find_firmware
17
- from .common import clean_version
14
+ from .flash_esp import flash_esp
15
+ from .flash_stm32 import flash_stm32
16
+ from .flash_uf2 import flash_uf2
17
+ from .mpboard_id.api import find_mp_board
18
18
  from .mpremoteboard import MPRemoteBoard
19
+
19
20
  # #########################################################################################################
20
21
  # CLI
21
22
  # #########################################################################################################
@@ -37,17 +38,18 @@ from .mpremoteboard import MPRemoteBoard
37
38
  @click.option(
38
39
  "--version",
39
40
  "-v",
40
- "target_version",
41
+ "version", # single version
41
42
  default="stable",
43
+ multiple=False,
42
44
  show_default=True,
43
45
  help="The version of MicroPython to flash.",
44
- metavar="SEMVER, stable or preview",
46
+ metavar="SEMVER, 'stable', 'preview' or '?'",
45
47
  )
46
48
  @click.option(
47
49
  "--serial",
48
50
  "--serial-port",
49
51
  "-s",
50
- "serial_port",
52
+ "serial",
51
53
  default="auto",
52
54
  show_default=True,
53
55
  help="Which serial port(s) to flash",
@@ -56,18 +58,20 @@ from .mpremoteboard import MPRemoteBoard
56
58
  @click.option(
57
59
  "--port",
58
60
  "-p",
59
- "port",
61
+ "ports",
60
62
  help="The MicroPython port to flash",
61
63
  metavar="PORT",
62
- default="",
64
+ default=[],
65
+ multiple=True,
63
66
  )
64
67
  @click.option(
65
68
  "--board",
66
69
  "-b",
67
- "board",
70
+ "boards",
71
+ multiple=False,
72
+ default=[],
68
73
  help="The MicroPython board ID to flash. If not specified will try to read the BOARD_ID from the connected MCU.",
69
- metavar="BOARD_ID",
70
- default="",
74
+ metavar="BOARD_ID or ?",
71
75
  )
72
76
  @click.option(
73
77
  "--cpu",
@@ -81,7 +85,7 @@ from .mpremoteboard import MPRemoteBoard
81
85
  "--erase/--no-erase",
82
86
  default=True,
83
87
  show_default=True,
84
- help="""Erase flash before writing new firmware. (not on UF2 boards)""",
88
+ help="""Erase flash before writing new firmware. (Not supported on UF2 boards)""",
85
89
  )
86
90
  @click.option(
87
91
  "--bootloader/--no-bootloader",
@@ -90,57 +94,108 @@ from .mpremoteboard import MPRemoteBoard
90
94
  show_default=True,
91
95
  help="""Enter micropython bootloader mode before flashing.""",
92
96
  )
93
- def cli_flash_board(
94
- target_version: str,
95
- fw_folder: Path,
96
- serial_port: Optional[str] = None,
97
- board: Optional[str] = None,
98
- port: Optional[str] = None,
99
- variant: Optional[str] = None,
100
- cpu: Optional[str] = None,
101
- erase: bool = False,
102
- # stm32_dfu: bool = True,
103
- bootloader: bool = True,
104
- ):
97
+ def cli_flash_board(**kwargs):
105
98
  todo: WorkList = []
106
- # firmware type selector
107
- selector = {
108
- "stm32": ".dfu", # if stm32_dfu else ".hex",
109
- }
110
- target_version = clean_version(target_version)
111
- preview = target_version == "preview"
112
- # Update all micropython boards to the latest version
113
- if target_version and port and board and serial_port:
114
- # TODO : Find a way to avoid needing to specify the port
115
- mcu = MPRemoteBoard(serial_port)
116
- mcu.port = port
117
- mcu.cpu = port if port.startswith("esp") else ""
118
- mcu.board = board
119
- firmwares = find_firmware(
120
- fw_folder=fw_folder,
121
- board=board,
122
- version=target_version,
123
- preview=target_version.lower() == "preview",
124
- port=port,
125
- selector=selector,
99
+
100
+ # version to versions
101
+ if "version" in kwargs:
102
+ kwargs["versions"] = [kwargs.pop("version")]
103
+ params = FlashParams(**kwargs)
104
+ print(f"{params=}")
105
+ # print(f"{params.version=}")
106
+ print(f"{params.versions=}")
107
+ if not params.boards:
108
+ # nothing specified - detect connected boards
109
+ params.ports, params.boards = connected_ports_boards()
110
+ # Ask for missing input if needed
111
+ params = ask_missing_params(params, action="flash")
112
+ # TODO: Just in time Download of firmware
113
+
114
+ assert isinstance(params, FlashParams)
115
+
116
+ if len(params.versions) > 1:
117
+ print(repr(params.versions))
118
+ log.error(f"Only one version can be flashed at a time, not {params.versions}")
119
+ return
120
+ params.versions = [clean_version(v) for v in params.versions]
121
+ if params.versions[0] and params.boards[0] and params.serial:
122
+ # update a single board
123
+ todo = manual_worklist(
124
+ params.versions[0],
125
+ params.fw_folder,
126
+ params.serial,
127
+ params.boards[0],
128
+ # params.ports[0],
126
129
  )
127
- if not firmwares:
128
- log.error(f"No firmware found for {port} {board} version {target_version}")
129
- return
130
- # use the most recent matching firmware
131
- todo = [(mcu, firmwares[-1])]
132
- elif serial_port:
133
- if serial_port == "auto":
134
- # update all connected boards
135
- conn_boards = [
136
- MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards() if sp not in config.ignore_ports
137
- ]
130
+ elif params.serial:
131
+ if params.serial == "auto":
132
+ # Update all micropython boards to the latest version
133
+ todo = auto_worklist(params.versions[0], params.fw_folder)
138
134
  else:
139
- # just this serial port
140
- conn_boards = [MPRemoteBoard(serial_port)]
141
- show_mcus(conn_boards)
142
- todo = auto_update(conn_boards, target_version, fw_folder, preview=preview, selector=selector)
135
+ # just this serial port on auto
136
+ todo = oneport_worklist(
137
+ params.versions[0],
138
+ params.fw_folder,
139
+ params.serial,
140
+ )
141
+
142
+ if flashed := flash_list(
143
+ todo,
144
+ params.fw_folder,
145
+ params.erase,
146
+ params.bootloader,
147
+ ):
148
+ log.info(f"Flashed {len(flashed)} boards")
149
+ show_mcus(flashed, title="Connected boards after flashing")
143
150
 
151
+
152
+ def oneport_worklist(
153
+ version: str,
154
+ fw_folder: Path,
155
+ serial_port: str,
156
+ # preview: bool,
157
+ ) -> WorkList:
158
+ """Create a worklist for a single serial-port."""
159
+ conn_boards = [MPRemoteBoard(serial_port)]
160
+ todo = auto_update(conn_boards, version, fw_folder) # type: ignore # List / list
161
+ show_mcus(conn_boards) # type: ignore
162
+ return todo
163
+
164
+
165
+ def auto_worklist(version: str, fw_folder: Path) -> WorkList:
166
+ conn_boards = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards() if sp not in config.ignore_ports]
167
+ return auto_update(conn_boards, version, fw_folder) # type: ignore
168
+
169
+
170
+ def manual_worklist(
171
+ version: str,
172
+ fw_folder: Path,
173
+ serial_port: str,
174
+ board: str,
175
+ # port: str,
176
+ ) -> WorkList:
177
+ mcu = MPRemoteBoard(serial_port)
178
+ # TODO : Find a way to avoid needing to specify the port
179
+ # Lookup the matching port and cpu in board_info based in the board name
180
+ port = find_mp_board(board)["port"]
181
+ mcu.port = port
182
+ mcu.cpu = port if port.startswith("esp") else ""
183
+ mcu.board = board
184
+ firmwares = find_firmware(fw_folder=fw_folder, board=board, version=version, port=port)
185
+ if not firmwares:
186
+ log.error(f"No firmware found for {port} {board} version {version}")
187
+ return []
188
+ # use the most recent matching firmware
189
+ return [(mcu, firmwares[-1])] # type: ignore
190
+
191
+
192
+ def flash_list(
193
+ todo: WorkList,
194
+ fw_folder: Path,
195
+ erase: bool,
196
+ bootloader: bool,
197
+ ):
198
+ """Flash a list of boards with the specified firmware."""
144
199
  flashed = []
145
200
  for mcu, fw_info in todo:
146
201
  fw_file = fw_folder / fw_info["filename"] # type: ignore
@@ -148,20 +203,19 @@ def cli_flash_board(
148
203
  log.error(f"File {fw_file} does not exist, skipping {mcu.board} on {mcu.serialport}")
149
204
  continue
150
205
  log.info(f"Updating {mcu.board} on {mcu.serialport} to {fw_info['version']}")
151
-
152
206
  updated = None
153
207
  # try:
154
- if mcu.port in ["samd", "rp2", "nrf"]:
208
+ if mcu.port in ["samd", "rp2", "nrf"]: # [k for k, v in PORT_FWTYPES.items() if v == ".uf2"]:
155
209
  if bootloader:
156
210
  enter_bootloader(mcu)
157
211
  updated = flash_uf2(mcu, fw_file=fw_file, erase=erase)
158
- elif mcu.port in ["esp32", "esp8266"]:
159
- # bootloader is handles by esptool for esp32/esp8266
160
- updated = flash_esp(mcu, fw_file=fw_file, erase=erase)
161
212
  elif mcu.port in ["stm32"]:
162
213
  if bootloader:
163
214
  enter_bootloader(mcu)
164
215
  updated = flash_stm32(mcu, fw_file, erase=erase)
216
+ elif mcu.port in ["esp32", "esp8266"]:
217
+ # bootloader is handled by esptool for esp32/esp8266
218
+ updated = flash_esp(mcu, fw_file=fw_file, erase=erase)
165
219
  else:
166
220
  log.error(f"Don't (yet) know how to flash {mcu.port}-{mcu.board} on {mcu.serialport}")
167
221
 
@@ -169,7 +223,3 @@ def cli_flash_board(
169
223
  flashed.append(updated)
170
224
  else:
171
225
  log.error(f"Failed to flash {mcu.board} on {mcu.serialport}")
172
-
173
- if flashed:
174
- log.info(f"Flashed {len(flashed)} boards")
175
- show_mcus(flashed, title="Connected boards after flashing")
mpflash/cli_group.py CHANGED
@@ -13,8 +13,10 @@ def cb_verbose(ctx, param, value):
13
13
  """Callback to set the log level to DEBUG if verbose is set"""
14
14
  if value:
15
15
  set_loglevel("DEBUG")
16
+ config.verbose = True
16
17
  else:
17
18
  set_loglevel("INFO")
19
+ config.verbose = False
18
20
  return value
19
21
 
20
22
 
@@ -24,6 +26,12 @@ def cb_ignore(ctx, param, value):
24
26
  return value
25
27
 
26
28
 
29
+ def cb_interactive(ctx, param, value):
30
+ if value:
31
+ config.interactive = value
32
+ return value
33
+
34
+
27
35
  def cb_quiet(ctx, param, value):
28
36
  if value:
29
37
  make_quiet()
@@ -31,6 +39,7 @@ def cb_quiet(ctx, param, value):
31
39
 
32
40
 
33
41
  @click.group()
42
+ @click.version_option(package_name="mpflash")
34
43
  @click.option(
35
44
  "--quiet",
36
45
  "-q",
@@ -41,6 +50,16 @@ def cb_quiet(ctx, param, value):
41
50
  envvar="MPFLASH_QUIET",
42
51
  show_default=True,
43
52
  )
53
+ @click.option(
54
+ "--interactive/--no-interactive",
55
+ "-i/-x",
56
+ is_eager=True,
57
+ help="Suppresses all request for Input.",
58
+ callback=cb_interactive,
59
+ # envvar="MPFLASH_QUIET",
60
+ default=True,
61
+ show_default=True,
62
+ )
44
63
  @click.option(
45
64
  "-V",
46
65
  "--verbose",
@@ -61,7 +80,7 @@ def cb_quiet(ctx, param, value):
61
80
  show_default=True,
62
81
  metavar="SERIALPORT",
63
82
  )
64
- def cli(quiet: bool, **kwargs):
83
+ def cli(**kwargs):
65
84
  """mpflash - MicroPython Tool.
66
85
 
67
86
  A CLI to download and flash MicroPython firmware to different ports and boards.
mpflash/cli_list.py CHANGED
@@ -6,10 +6,11 @@ from rich import print
6
6
  from rich.progress import track
7
7
  from rich.table import Table
8
8
 
9
+ from mpflash.mpremoteboard import MPRemoteBoard
10
+
9
11
  from .cli_group import cli
10
12
  from .config import config
11
13
  from .logger import console, make_quiet
12
- from .mpremoteboard import MPRemoteBoard
13
14
 
14
15
 
15
16
  @cli.command("list", help="List the connected MCU boards.")
@@ -67,8 +68,8 @@ def show_mcus(
67
68
  title=title,
68
69
  header_style="bold blue",
69
70
  collapse_padding=True,
70
- width=100,
71
- # row_styles=["blue", "yellow"]
71
+ width=110,
72
+ row_styles=["blue", "yellow"],
72
73
  )
73
74
  table.add_column("Serial", overflow="fold")
74
75
  table.add_column("Family")
@@ -89,7 +90,7 @@ def show_mcus(
89
90
  mcu.serialport.replace("/dev/", ""),
90
91
  mcu.family,
91
92
  mcu.port,
92
- mcu.board if mcu.board != "UNKNOWN" else mcu.description,
93
+ f"{mcu.board}\n{mcu.description}".strip(),
93
94
  # mcu.variant,
94
95
  mcu.cpu,
95
96
  mcu.version,
mpflash/cli_main.py CHANGED
@@ -3,8 +3,8 @@
3
3
  # import rich_click as click
4
4
 
5
5
  from .cli_download import cli_download
6
- from .cli_group import cli
7
6
  from .cli_flash import cli_flash_board
7
+ from .cli_group import cli
8
8
  from .cli_list import cli_list_mcus
9
9
 
10
10
  # from loguru import logger as log
@@ -14,8 +14,9 @@ def mpflash():
14
14
  cli.add_command(cli_flash_board)
15
15
  cli.add_command(cli_list_mcus)
16
16
  cli.add_command(cli_download)
17
- cli(auto_envvar_prefix="MPFLASH")
17
+ # cli(auto_envvar_prefix="MPFLASH")
18
+ cli()
18
19
 
19
20
 
20
- if __name__ == "__main__":
21
- mpflash()
21
+ # if __name__ == "__main__":
22
+ mpflash()