mpflash 0.4.0.post2__py3-none-any.whl → 0.4.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/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
@@ -5,11 +5,13 @@ from typing import List, Tuple
5
5
 
6
6
  import rich_click as click
7
7
 
8
+ from mpflash.common import clean_version
9
+
10
+ from .ask_input import DownloadParams, ask_missing_params
8
11
  from .cli_group import cli
12
+ from .cli_list import list_mcus
9
13
  from .config import config
10
14
  from .download import download
11
- from .download_input import DownloadParams, ask_missing_params
12
- from .cli_list import list_mcus
13
15
 
14
16
 
15
17
  def connected_ports_boards() -> Tuple[List[str], List[str]]:
@@ -26,6 +28,7 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
26
28
  @click.option(
27
29
  "--destination",
28
30
  "-d",
31
+ "fw_folder",
29
32
  type=click.Path(file_okay=False, dir_okay=True, path_type=Path),
30
33
  default=config.firmware_folder,
31
34
  show_default=True,
@@ -35,18 +38,21 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
35
38
  "--version",
36
39
  "-v",
37
40
  "versions",
41
+ default=["stable"],
38
42
  multiple=True,
39
- help="The version of MicroPython to to download. Use 'preview' to include preview versions.",
40
43
  show_default=True,
41
- default=["stable"],
44
+ help="The version of MicroPython to to download.",
45
+ metavar="SEMVER, 'stable', 'preview' or '?'",
42
46
  )
43
47
  @click.option(
44
48
  "--board",
45
49
  "-b",
46
50
  "boards",
47
51
  multiple=True,
52
+ default=["?"],
48
53
  show_default=True,
49
- 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 ?",
50
56
  )
51
57
  @click.option(
52
58
  "--clean/--no-clean",
@@ -58,8 +64,8 @@ def connected_ports_boards() -> Tuple[List[str], List[str]]:
58
64
  "--force",
59
65
  default=False,
60
66
  is_flag=True,
61
- help="""Force download of firmware even if it already exists.""",
62
67
  show_default=True,
68
+ help="""Force download of firmware even if it already exists.""",
63
69
  )
64
70
  def cli_download(
65
71
  **kwargs,
@@ -70,14 +76,16 @@ def cli_download(
70
76
  # nothing specified - detect connected boards
71
77
  params.ports, params.boards = connected_ports_boards()
72
78
  # ask for any remaining parameters
73
- params = ask_missing_params(params)
74
- # preview is not a version, it is an option to include preview versions
79
+ params = ask_missing_params(params, action="download")
80
+ assert isinstance(params, DownloadParams)
81
+
82
+ params.versions = [clean_version(v, drop_v=True) for v in params.versions] # remove leading v from version
83
+
75
84
  download(
76
- params.destination,
85
+ params.fw_folder,
77
86
  params.ports,
78
87
  params.boards,
79
88
  params.versions,
80
89
  params.force,
81
90
  params.clean,
82
- params.preview,
83
91
  )
mpflash/cli_flash.py CHANGED
@@ -1,21 +1,22 @@
1
-
2
1
  from pathlib import Path
3
- from typing import Optional
4
-
5
2
 
6
3
  import rich_click as click
7
4
  from loguru import logger as log
8
5
 
9
- from .cli_group import cli
6
+ from mpflash.mpboard_id.api import find_mp_board
10
7
 
11
- from .flash_esp import flash_esp
12
- from .flash_stm32 import flash_stm32
13
- from .flash_uf2 import flash_uf2
8
+ from .ask_input import FlashParams, ask_missing_params
9
+ from .cli_download import connected_ports_boards
10
+ from .cli_group import cli
14
11
  from .cli_list import show_mcus
12
+ from .common import clean_version
15
13
  from .config import config
16
14
  from .flash import WorkList, auto_update, enter_bootloader, find_firmware
17
- from .common import clean_version
15
+ from .flash_esp import flash_esp
16
+ from .flash_stm32 import flash_stm32
17
+ from .flash_uf2 import flash_uf2
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",
@@ -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")
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)
161
+ show_mcus(conn_boards)
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)
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])]
190
+
143
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()