mpflash 0.5.0__py3-none-any.whl → 0.7.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/list.py ADDED
@@ -0,0 +1,88 @@
1
+ from typing import List
2
+
3
+ from rich import print
4
+ from rich.progress import BarColumn, Progress, SpinnerColumn, TextColumn, TimeElapsedColumn, track
5
+ from rich.table import Column, Table
6
+
7
+ from mpflash.mpremoteboard import MPRemoteBoard
8
+
9
+ from .config import config
10
+ from .logger import console
11
+
12
+ rp_spinner = SpinnerColumn(finished_text="✅")
13
+ rp_text = TextColumn("{task.description} {task.fields[device]}", table_column=Column())
14
+ rp_bar = BarColumn(bar_width=None, table_column=Column())
15
+
16
+
17
+ def list_mcus(bluetooth: bool = False):
18
+ """
19
+ Retrieves information about connected microcontroller boards.
20
+
21
+ Returns:
22
+ List[MPRemoteBoard]: A list of MPRemoteBoard instances with board information.
23
+ Raises:
24
+ ConnectionError: If there is an error connecting to a board.
25
+ """
26
+ conn_mcus = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards(bluetooth) if sp not in config.ignore_ports]
27
+
28
+ # a lot of boilerplate to show a progress bar with the comport currenlty scanned
29
+ with Progress(rp_spinner, rp_text, rp_bar, TimeElapsedColumn()) as progress:
30
+ tsk_scan = progress.add_task("[green]Scanning", visible=False, total=None)
31
+ progress.tasks[tsk_scan].fields["device"] = "..."
32
+ progress.tasks[tsk_scan].visible = True
33
+ progress.start_task(tsk_scan)
34
+ try:
35
+ for mcu in conn_mcus:
36
+ progress.update(tsk_scan, device=mcu.serialport.replace("/dev/", ""))
37
+ try:
38
+ mcu.get_mcu_info()
39
+ except ConnectionError as e:
40
+ print(f"Error: {e}")
41
+ continue
42
+ finally:
43
+ # transient
44
+ progress.stop_task(tsk_scan)
45
+ progress.tasks[tsk_scan].visible = False
46
+ return conn_mcus
47
+
48
+
49
+ def show_mcus(
50
+ conn_mcus: List[MPRemoteBoard],
51
+ title: str = "Connected boards",
52
+ refresh: bool = True,
53
+ ): # sourcery skip: extract-duplicate-method
54
+ """Show the list of connected boards in a nice table"""
55
+ table = Table(
56
+ title=title,
57
+ title_style="magenta",
58
+ header_style="bold magenta",
59
+ collapse_padding=True,
60
+ width=110,
61
+ )
62
+ table.add_column("Serial", overflow="fold")
63
+ table.add_column("Family")
64
+ table.add_column("Port")
65
+ table.add_column("Board", overflow="fold")
66
+ # table.add_column("Variant") # TODO: add variant
67
+ table.add_column("CPU")
68
+ table.add_column("Version")
69
+ table.add_column("build", justify="right")
70
+
71
+ for mcu in track(conn_mcus, description="Updating board info", transient=True, update_period=0.1):
72
+ if refresh:
73
+ try:
74
+ mcu.get_mcu_info()
75
+ except ConnectionError:
76
+ continue
77
+ description = f"[italic bright_cyan]{mcu.description}" if mcu.description else ""
78
+ table.add_row(
79
+ mcu.serialport.replace("/dev/", ""),
80
+ mcu.family,
81
+ mcu.port,
82
+ f"{mcu.board}\n{description}".strip(),
83
+ # mcu.variant,
84
+ mcu.cpu,
85
+ mcu.version,
86
+ mcu.build,
87
+ )
88
+ console.print(table)
@@ -1,9 +1,17 @@
1
+ """
2
+ Access to the micropython port and board information that is stored in the board_info.json file
3
+ that is included in the module.
4
+
5
+ """
6
+
1
7
  import json
2
8
  from functools import lru_cache
3
9
  from pathlib import Path
4
10
  from typing import List, Optional, Tuple, TypedDict, Union
5
11
 
6
- from mpflash.common import PORT_FWTYPES, clean_version
12
+ from mpflash.errors import MPFlashError
13
+ from mpflash.common import PORT_FWTYPES
14
+ from mpflash.vendor.versions import clean_version
7
15
 
8
16
 
9
17
  # Board based on the dataclass Board but changed to TypedDict
@@ -22,27 +30,27 @@ class Board(TypedDict):
22
30
 
23
31
 
24
32
  @lru_cache(maxsize=None)
25
- def read_boardinfo() -> List[Board]:
33
+ def read_stored_boardinfo() -> List[Board]:
26
34
  """Reads the board_info.json file and returns the data as a list of Board objects"""
27
35
  with open(Path(__file__).parent / "board_info.json", "r") as file:
28
36
  return json.load(file)
29
37
 
30
38
 
31
- def known_mp_ports() -> List[str]:
39
+ def local_mp_ports() -> List[str]:
32
40
  # TODO: Filter for Version
33
- mp_boards = read_boardinfo()
41
+ mp_boards = read_stored_boardinfo()
34
42
  # select the unique ports from info
35
43
  ports = set({board["port"] for board in mp_boards if board["port"] in PORT_FWTYPES.keys()})
36
44
  return sorted(list(ports))
37
45
 
38
46
 
39
- def get_mp_boards_for_port(port: str, versions: Optional[List[str]] = None):
47
+ def get_stored_boards_for_port(port: str, versions: Optional[List[str]] = None):
40
48
  """
41
49
  Returns a list of boards for the given port and version(s)
42
50
 
43
51
  port : str : The Micropython port to filter for
44
52
  versions : List[str] : The Micropython versions to filter for (actual versions required)"""
45
- mp_boards = read_boardinfo()
53
+ mp_boards = read_stored_boardinfo()
46
54
 
47
55
  # filter for 'preview' as they are not in the board_info.json
48
56
  # instead use stable version
@@ -60,30 +68,29 @@ def get_mp_boards_for_port(port: str, versions: Optional[List[str]] = None):
60
68
  return mp_boards
61
69
 
62
70
 
63
- def known_mp_boards(port: str, versions: Optional[List[str]] = None) -> List[Tuple[str, str]]:
71
+ def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List[Tuple[str, str]]:
64
72
  """
65
73
  Returns a list of tuples with the description and board name for the given port and version
66
74
 
67
75
  port : str : The Micropython port to filter for
68
76
  versions : List[str] : The Micropython versions to filter for (actual versions required)
69
77
  """
70
- mp_boards = get_mp_boards_for_port(port, versions)
78
+ mp_boards = get_stored_boards_for_port(port, versions)
71
79
 
72
- boards = set(
73
- {(f'{board["description"]} [board["board"]] {board["version"]}', board["board"]) for board in mp_boards}
74
- )
80
+ boards = set({(f'{board["version"]} {board["description"]}', board["board"]) for board in mp_boards})
75
81
  return sorted(list(boards))
76
82
 
77
83
 
78
- def find_mp_board(board: str) -> Board:
79
- """Find the board for the given board"""
80
- info = read_boardinfo()
84
+ @lru_cache(maxsize=20)
85
+ def find_stored_board(board_id: str) -> Board:
86
+ """Find the board for the given board_ID or 'board description' and return the board info as a Board object"""
87
+ info = read_stored_boardinfo()
81
88
  for board_info in info:
82
- if board_info["board"] == board:
89
+ if board_id in (board_info["board"], board_info["description"]):
83
90
  if "cpu" not in board_info or not board_info["cpu"]:
84
91
  if " with " in board_info["description"]:
85
92
  board_info["cpu"] = board_info["description"].split(" with ")[-1]
86
93
  else:
87
94
  board_info["cpu"] = board_info["port"]
88
95
  return board_info
89
- raise LookupError(f"Board {board} not found")
96
+ raise MPFlashError(f"Board {board_id} not found")
@@ -2,44 +2,62 @@
2
2
  Translate board description to board designator
3
3
  """
4
4
 
5
+ import functools
6
+ import json
5
7
  from pathlib import Path
6
8
  from typing import Optional
7
9
 
10
+ from mpflash.errors import MPFlashError
11
+ from mpflash.vendor.versions import clean_version
12
+
8
13
  ###############################################################################################
9
- # TODO : make this a bit nicer
10
14
  HERE = Path(__file__).parent
11
15
  ###############################################################################################
12
16
 
13
17
 
14
- def find_board_designator(descr: str, short_descr: str, board_info: Optional[Path] = None) -> Optional[str]:
18
+ def find_board_id(
19
+ descr: str, short_descr: str, board_info: Optional[Path] = None, version: str = "stable"
20
+ ) -> Optional[str]:
15
21
  # TODO: use the json file instead of the csv and get the cpu
16
- return find_board_designator_csv(descr, short_descr, board_info)
22
+ boards = find_board_by_description(
23
+ descr=descr,
24
+ short_descr=short_descr,
25
+ board_info=board_info,
26
+ version=clean_version(version),
27
+ )
28
+ return boards[-1]["board"]
17
29
 
18
30
 
19
- def find_board_designator_csv(descr: str, short_descr: str, board_info: Optional[Path] = None) -> Optional[str]:
31
+ @functools.lru_cache(maxsize=20)
32
+ def find_board_by_description(*, descr: str, short_descr: str, version="v1.21.0", board_info: Optional[Path] = None):
20
33
  """
21
34
  Find the MicroPython BOARD designator based on the description in the firmware
22
- using the pre-built board_info.csv file
35
+ using the pre-built board_info.json file
23
36
  """
24
37
  if not board_info:
25
- board_info = HERE / "board_info.csv"
38
+ board_info = HERE / "board_info.json"
26
39
  if not board_info.exists():
27
40
  raise FileNotFoundError(f"Board info file not found: {board_info}")
28
41
 
29
- short_hit = ""
42
+ info = _read_board_info(board_info)
43
+
44
+ # filter for matching version
45
+ if version == "preview":
46
+ # TODO: match last stable
47
+ version = "v1.22.2"
48
+ version_matches = [b for b in info if b["version"].startswith(version)]
49
+ if not version_matches:
50
+ raise MPFlashError(f"No board info found for version {version}")
51
+ matches = [b for b in version_matches if b["description"] == descr]
52
+ if not matches and short_descr:
53
+ matches = [b for b in version_matches if b["description"] == short_descr]
54
+ if not matches:
55
+ raise MPFlashError(f"No board info found for description {descr}")
56
+ return sorted(matches, key=lambda x: x["version"])
57
+
58
+
59
+ @functools.lru_cache(maxsize=20)
60
+ def _read_board_info(board_info):
30
61
  with open(board_info, "r") as file:
31
- while 1:
32
- line = file.readline()
33
- if not line:
34
- break
35
- descr_, board_ = line.split(",")[0].strip(), line.split(",")[1].strip()
36
- if descr_ == descr:
37
- return board_
38
- if short_descr and descr_ == short_descr:
39
- if "with" in short_descr:
40
- # Good enough - no need to trawl the entire file
41
- # info["board"] = board_
42
- return board_
43
- # good enough if not found in the rest of the file (but slow)
44
- short_hit = board_
45
- return short_hit or None
62
+ info = json.load(file)
63
+ return info
@@ -3,16 +3,18 @@ Module to run mpremote commands, and retry on failure or timeout
3
3
  """
4
4
 
5
5
  import sys
6
+ import time
6
7
  from pathlib import Path
7
8
  from typing import List, Optional, Union
8
9
 
9
10
  import serial.tools.list_ports
10
11
  from loguru import logger as log
12
+ from rich.progress import track
11
13
  from tenacity import retry, stop_after_attempt, wait_fixed
12
14
 
13
- from mpflash.mpboard_id.board_id import find_board_designator
14
-
15
- from .runner import run
15
+ from mpflash.errors import MPFlashError
16
+ from mpflash.mpboard_id.board_id import find_board_id
17
+ from mpflash.mpremoteboard.runner import run
16
18
 
17
19
  ###############################################################################################
18
20
  # TODO : make this a bit nicer
@@ -27,9 +29,15 @@ RETRIES = 3
27
29
  class MPRemoteBoard:
28
30
  """Class to run mpremote commands"""
29
31
 
30
- def __init__(self, serialport: str = ""):
32
+ def __init__(self, serialport: str = "", update: bool = False):
33
+ """
34
+ Initialize MPRemoteBoard object.
35
+
36
+ Parameters:
37
+ - serialport (str): The serial port to connect to. Default is an empty string.
38
+ - update (bool): Whether to update the MCU information. Default is False.
39
+ """
31
40
  self.serialport = serialport
32
- # self.board = ""
33
41
  self.firmware = {}
34
42
 
35
43
  self.connected = False
@@ -43,18 +51,50 @@ class MPRemoteBoard:
43
51
  self.arch = ""
44
52
  self.mpy = ""
45
53
  self.build = ""
54
+ if update:
55
+ self.get_mcu_info()
46
56
 
47
57
  def __str__(self):
58
+ """
59
+ Return a string representation of the MPRemoteBoard object.
60
+
61
+ Returns:
62
+ - str: The string representation of the object.
63
+ """
48
64
  return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}, {self.version})"
49
65
 
50
66
  @staticmethod
51
- def connected_boards():
52
- """Get a list of connected boards"""
53
- devices = [p.device for p in serial.tools.list_ports.comports()]
54
- return sorted(devices)
67
+ def connected_boards(bluetooth: bool = False) -> List[str]:
68
+ # TODO: rename to connected_comports
69
+ """
70
+ Get a list of connected comports.
71
+
72
+ Parameters:
73
+ - bluetooth (bool): Whether to include Bluetooth ports. Default is False.
74
+
75
+ Returns:
76
+ - List[str]: A list of connected board ports.
77
+ """
78
+ comports = serial.tools.list_ports.comports()
79
+
80
+ if not bluetooth:
81
+ # filter out bluetooth ports
82
+ comports = [p for p in comports if "bluetooth" not in p.description.lower()]
83
+ comports = [p for p in comports if "BTHENUM" not in p.hwid]
84
+
85
+ return sorted([p.device for p in comports])
86
+
87
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
88
+ def get_mcu_info(self, timeout: int = 2):
89
+ """
90
+ Get MCU information from the connected board.
91
+
92
+ Parameters:
93
+ - timeout (int): The timeout value in seconds. Default is 2.
55
94
 
56
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), retry_error_cls=ConnectionError) # type: ignore
57
- def get_mcu_info(self, timeout: int = 6):
95
+ Raises:
96
+ - ConnectionError: If failed to get mcu_info for the serial port.
97
+ """
58
98
  rc, result = self.run_command(
59
99
  ["run", str(HERE / "mpy_fw_info.py")],
60
100
  no_info=True,
@@ -76,13 +116,18 @@ class MPRemoteBoard:
76
116
  self.description = descr = info["board"]
77
117
  pos = descr.rfind(" with")
78
118
  short_descr = descr[:pos].strip() if pos != -1 else ""
79
- if board_name := find_board_designator(descr, short_descr):
119
+ if board_name := find_board_id(descr, short_descr):
80
120
  self.board = board_name
81
121
  else:
82
122
  self.board = "UNKNOWN"
83
123
 
84
124
  def disconnect(self) -> bool:
85
- """Disconnect from a board"""
125
+ """
126
+ Disconnect from a board.
127
+
128
+ Returns:
129
+ - bool: True if successfully disconnected, False otherwise.
130
+ """
86
131
  if not self.connected:
87
132
  return True
88
133
  if not self.serialport:
@@ -94,7 +139,7 @@ class MPRemoteBoard:
94
139
  self.connected = False
95
140
  return result
96
141
 
97
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2))
142
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2), reraise=True)
98
143
  def run_command(
99
144
  self,
100
145
  cmd: Union[str, List[str]],
@@ -104,21 +149,23 @@ class MPRemoteBoard:
104
149
  timeout: int = 60,
105
150
  **kwargs,
106
151
  ):
107
- """Run mpremote with the given command
108
- Parameters
109
- ----------
110
- cmd : Union[str,List[str]]
111
- The command to run, either a string or a list of strings
112
- check : bool, optional
113
- If True, raise an exception if the command fails, by default False
114
- Returns
115
- -------
116
- bool
117
- True if the command succeeded, False otherwise
152
+ """
153
+ Run mpremote with the given command.
154
+
155
+ Parameters:
156
+ - cmd (Union[str, List[str]]): The command to run, either a string or a list of strings.
157
+ - log_errors (bool): Whether to log errors. Default is True.
158
+ - no_info (bool): Whether to skip printing info. Default is False.
159
+ - timeout (int): The timeout value in seconds. Default is 60.
160
+
161
+ Returns:
162
+ - bool: True if the command succeeded, False otherwise.
118
163
  """
119
164
  if isinstance(cmd, str):
120
165
  cmd = cmd.split(" ")
121
- prefix = [sys.executable, "-m", "mpremote", "connect", self.serialport] if self.serialport else ["mpremote"]
166
+ prefix = [sys.executable, "-m", "mpremote"]
167
+ if self.serialport:
168
+ prefix += ["connect", self.serialport]
122
169
  # if connected add resume to keep state between commands
123
170
  if self.connected:
124
171
  prefix += ["resume"]
@@ -130,9 +177,33 @@ class MPRemoteBoard:
130
177
 
131
178
  @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1))
132
179
  def mip_install(self, name: str) -> bool:
133
- """Install a micropython package"""
180
+ """
181
+ Install a micropython package.
182
+
183
+ Parameters:
184
+ - name (str): The name of the package to install.
185
+
186
+ Returns:
187
+ - bool: True if the installation succeeded, False otherwise.
188
+ """
134
189
  # install createstubs to the board
135
190
  cmd = ["mip", "install", name]
136
191
  result = self.run_command(cmd)[0] == OK
137
192
  self.connected = True
138
193
  return result
194
+
195
+ def wait_for_restart(self, timeout: int = 10):
196
+ """wait for the board to restart"""
197
+ for _ in track(
198
+ range(timeout),
199
+ description="Waiting for the board to restart",
200
+ transient=True,
201
+ get_time=lambda: time.time(),
202
+ show_speed=False,
203
+ ):
204
+ time.sleep(1)
205
+ try:
206
+ self.get_mcu_info()
207
+ break
208
+ except (ConnectionError, MPFlashError):
209
+ pass
@@ -27,6 +27,7 @@ def run(
27
27
  log_errors: bool = True,
28
28
  no_info: bool = False,
29
29
  *,
30
+ log_warnings: bool = False,
30
31
  reset_tags: Optional[LogTagList] = None,
31
32
  error_tags: Optional[LogTagList] = None,
32
33
  warning_tags: Optional[LogTagList] = None,
@@ -93,7 +94,8 @@ def run(
93
94
 
94
95
  def timed_out():
95
96
  proc.kill()
96
- log.warning(f"Command {cmd} timed out after {timeout} seconds")
97
+ if log_warnings:
98
+ log.warning(f"Command {cmd} timed out after {timeout} seconds")
97
99
 
98
100
  timer = Timer(timeout, timed_out)
99
101
  try:
@@ -123,6 +125,8 @@ def run(
123
125
  continue
124
126
  else:
125
127
  if not no_info:
128
+ if line.startswith(("INFO : ", "WARN : ", "ERROR : ")):
129
+ line = line[8:].lstrip()
126
130
  log.info(line)
127
131
  if proc.stderr and log_errors:
128
132
  for line in proc.stderr:
@@ -0,0 +1,113 @@
1
+ """
2
+ #############################################################
3
+ # Version handling copied from stubber/utils/versions.py
4
+ #############################################################
5
+ """
6
+
7
+ from functools import lru_cache
8
+
9
+ from loguru import logger as log
10
+ from packaging.version import parse
11
+
12
+ V_PREVIEW = "preview"
13
+ "Latest preview version"
14
+
15
+ SET_PREVIEW = {"preview", "latest", "master"}
16
+
17
+
18
+ def clean_version(
19
+ version: str,
20
+ *,
21
+ build: bool = False,
22
+ patch: bool = False,
23
+ commit: bool = False,
24
+ drop_v: bool = False,
25
+ flat: bool = False,
26
+ ):
27
+ "Clean up and transform the many flavours of versions"
28
+ # 'v1.13.0-103-gb137d064e' --> 'v1.13-103'
29
+ if version in {"", "-"}:
30
+ return version
31
+ if version.lower() == "stable":
32
+ _v = get_stable_mp_version()
33
+ if not _v:
34
+ log.warning("Could not determine the latest stable version")
35
+ return "stable"
36
+ version = _v
37
+ log.trace(f"Using latest stable version: {version}")
38
+ is_preview = "-preview" in version
39
+ nibbles = version.split("-")
40
+ ver_ = nibbles[0].lower().lstrip("v")
41
+ if not patch and ver_ >= "1.10.0" and ver_ < "1.20.0" and ver_.endswith(".0"):
42
+ # remove the last ".0" - but only for versions between 1.10 and 1.20 (because)
43
+ nibbles[0] = nibbles[0][:-2]
44
+ if len(nibbles) == 1:
45
+ version = nibbles[0]
46
+ elif build and not is_preview:
47
+ version = "-".join(nibbles) if commit else "-".join(nibbles[:-1])
48
+ else:
49
+ # version = "-".join((nibbles[0], LATEST))
50
+ # HACK: this is not always right, but good enough most of the time
51
+ if is_preview:
52
+ version = "-".join((nibbles[0], V_PREVIEW))
53
+ else:
54
+ version = V_PREVIEW
55
+ if flat:
56
+ version = version.strip().replace(".", "_").replace("-", "_")
57
+ else:
58
+ version = version.strip().replace("_preview", "-preview").replace("_", ".")
59
+
60
+ if drop_v:
61
+ version = version.lstrip("v")
62
+ elif not version.startswith("v") and version.lower() not in SET_PREVIEW:
63
+ version = "v" + version
64
+ if version in SET_PREVIEW:
65
+ version = V_PREVIEW
66
+ return version
67
+
68
+
69
+ @lru_cache(maxsize=10)
70
+ def micropython_versions(minver: str = "v1.20"):
71
+ """Get the list of micropython versions from github tags"""
72
+ try:
73
+ gh_client = GH_CLIENT
74
+ repo = gh_client.get_repo("micropython/micropython")
75
+ versions = [tag.name for tag in repo.get_tags() if parse(tag.name) >= parse(minver)]
76
+ except Exception:
77
+ versions = [
78
+ "v9.99.9-preview",
79
+ "v1.22.2",
80
+ "v1.22.1",
81
+ "v1.22.0",
82
+ "v1.21.1",
83
+ "v1.21.0",
84
+ "v1.20.0",
85
+ "v1.19.1",
86
+ "v1.19",
87
+ "v1.18",
88
+ "v1.17",
89
+ "v1.16",
90
+ "v1.15",
91
+ "v1.14",
92
+ "v1.13",
93
+ "v1.12",
94
+ "v1.11",
95
+ "v1.10",
96
+ ]
97
+ versions = [v for v in versions if parse(v) >= parse(minver)]
98
+ return sorted(versions)
99
+
100
+
101
+ def get_stable_mp_version() -> str:
102
+ # read the versions from the git tags
103
+ all_versions = micropython_versions(minver="v1.17")
104
+ return [v for v in all_versions if not v.endswith(V_PREVIEW)][-1]
105
+
106
+
107
+ def get_preview_mp_version() -> str:
108
+ # read the versions from the git tags
109
+ all_versions = micropython_versions(minver="v1.17")
110
+ return [v for v in all_versions if v.endswith(V_PREVIEW)][-1]
111
+
112
+
113
+ #############################################################