mpflash 0.4.2__py3-none-any.whl → 0.6.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,72 @@
1
+ from typing import List
2
+
3
+ from rich import print
4
+ from rich.progress import track
5
+ from rich.table import Table
6
+
7
+ from mpflash.mpremoteboard import MPRemoteBoard
8
+
9
+ from .config import config
10
+ from .logger import console
11
+
12
+
13
+ def list_mcus(bluetooth: bool = False):
14
+ """
15
+ Retrieves information about connected microcontroller boards.
16
+
17
+ Returns:
18
+ List[MPRemoteBoard]: A list of MPRemoteBoard instances with board information.
19
+ Raises:
20
+ ConnectionError: If there is an error connecting to a board.
21
+ """
22
+ conn_mcus = [MPRemoteBoard(sp) for sp in MPRemoteBoard.connected_boards(bluetooth) if sp not in config.ignore_ports]
23
+
24
+ for mcu in track(conn_mcus, description="Getting board info", transient=True, update_period=0.1):
25
+ try:
26
+ mcu.get_mcu_info()
27
+ except ConnectionError as e:
28
+ print(f"Error: {e}")
29
+ continue
30
+ return conn_mcus
31
+
32
+
33
+ def show_mcus(
34
+ conn_mcus: List[MPRemoteBoard],
35
+ title: str = "Connected boards",
36
+ refresh: bool = True,
37
+ ): # sourcery skip: extract-duplicate-method
38
+ """Show the list of connected boards in a nice table"""
39
+ table = Table(
40
+ title=title,
41
+ title_style="bold",
42
+ header_style="bold blue",
43
+ collapse_padding=True,
44
+ width=110,
45
+ row_styles=["blue", "yellow"],
46
+ )
47
+ table.add_column("Serial", overflow="fold")
48
+ table.add_column("Family")
49
+ table.add_column("Port")
50
+ table.add_column("Board", overflow="fold")
51
+ # table.add_column("Variant") # TODO: add variant
52
+ table.add_column("CPU")
53
+ table.add_column("Version")
54
+ table.add_column("build", justify="right")
55
+
56
+ for mcu in track(conn_mcus, description="Updating board info", transient=True, update_period=0.1):
57
+ if refresh:
58
+ try:
59
+ mcu.get_mcu_info()
60
+ except ConnectionError:
61
+ continue
62
+ table.add_row(
63
+ mcu.serialport.replace("/dev/", ""),
64
+ mcu.family,
65
+ mcu.port,
66
+ f"{mcu.board}\n{mcu.description}".strip(),
67
+ # mcu.variant,
68
+ mcu.cpu,
69
+ mcu.version,
70
+ mcu.build,
71
+ )
72
+ 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,49 @@ 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
+ """
69
+ Get a list of connected boards.
70
+
71
+ Parameters:
72
+ - bluetooth (bool): Whether to include Bluetooth ports. Default is False.
73
+
74
+ Returns:
75
+ - List[str]: A list of connected board ports.
76
+ """
77
+ ports = serial.tools.list_ports.comports()
78
+
79
+ if not bluetooth:
80
+ # filter out bluetooth ports
81
+ ports = [p for p in ports if "bluetooth" not in p.description.lower()]
82
+ ports = [p for p in ports if "BTHENUM" not in p.hwid]
83
+
84
+ return sorted([p.device for p in ports])
85
+
86
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1), reraise=True) # type: ignore ## retry_error_cls=ConnectionError,
87
+ def get_mcu_info(self, timeout: int = 2):
88
+ """
89
+ Get MCU information from the connected board.
90
+
91
+ Parameters:
92
+ - timeout (int): The timeout value in seconds. Default is 2.
55
93
 
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):
94
+ Raises:
95
+ - ConnectionError: If failed to get mcu_info for the serial port.
96
+ """
58
97
  rc, result = self.run_command(
59
98
  ["run", str(HERE / "mpy_fw_info.py")],
60
99
  no_info=True,
@@ -76,13 +115,18 @@ class MPRemoteBoard:
76
115
  self.description = descr = info["board"]
77
116
  pos = descr.rfind(" with")
78
117
  short_descr = descr[:pos].strip() if pos != -1 else ""
79
- if board_name := find_board_designator(descr, short_descr):
118
+ if board_name := find_board_id(descr, short_descr):
80
119
  self.board = board_name
81
120
  else:
82
121
  self.board = "UNKNOWN"
83
122
 
84
123
  def disconnect(self) -> bool:
85
- """Disconnect from a board"""
124
+ """
125
+ Disconnect from a board.
126
+
127
+ Returns:
128
+ - bool: True if successfully disconnected, False otherwise.
129
+ """
86
130
  if not self.connected:
87
131
  return True
88
132
  if not self.serialport:
@@ -94,7 +138,7 @@ class MPRemoteBoard:
94
138
  self.connected = False
95
139
  return result
96
140
 
97
- @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2))
141
+ @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(2), reraise=True)
98
142
  def run_command(
99
143
  self,
100
144
  cmd: Union[str, List[str]],
@@ -104,21 +148,23 @@ class MPRemoteBoard:
104
148
  timeout: int = 60,
105
149
  **kwargs,
106
150
  ):
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
151
+ """
152
+ Run mpremote with the given command.
153
+
154
+ Parameters:
155
+ - cmd (Union[str, List[str]]): The command to run, either a string or a list of strings.
156
+ - log_errors (bool): Whether to log errors. Default is True.
157
+ - no_info (bool): Whether to skip printing info. Default is False.
158
+ - timeout (int): The timeout value in seconds. Default is 60.
159
+
160
+ Returns:
161
+ - bool: True if the command succeeded, False otherwise.
118
162
  """
119
163
  if isinstance(cmd, str):
120
164
  cmd = cmd.split(" ")
121
- prefix = [sys.executable, "-m", "mpremote", "connect", self.serialport] if self.serialport else ["mpremote"]
165
+ prefix = [sys.executable, "-m", "mpremote"]
166
+ if self.serialport:
167
+ prefix += ["connect", self.serialport]
122
168
  # if connected add resume to keep state between commands
123
169
  if self.connected:
124
170
  prefix += ["resume"]
@@ -130,9 +176,33 @@ class MPRemoteBoard:
130
176
 
131
177
  @retry(stop=stop_after_attempt(RETRIES), wait=wait_fixed(1))
132
178
  def mip_install(self, name: str) -> bool:
133
- """Install a micropython package"""
179
+ """
180
+ Install a micropython package.
181
+
182
+ Parameters:
183
+ - name (str): The name of the package to install.
184
+
185
+ Returns:
186
+ - bool: True if the installation succeeded, False otherwise.
187
+ """
134
188
  # install createstubs to the board
135
189
  cmd = ["mip", "install", name]
136
190
  result = self.run_command(cmd)[0] == OK
137
191
  self.connected = True
138
192
  return result
193
+
194
+ def wait_for_restart(self, timeout: int = 10):
195
+ """wait for the board to restart"""
196
+ for _ in track(
197
+ range(timeout),
198
+ description="Waiting for the board to restart",
199
+ transient=True,
200
+ get_time=lambda: time.time(),
201
+ show_speed=False,
202
+ ):
203
+ time.sleep(1)
204
+ try:
205
+ self.get_mcu_info()
206
+ break
207
+ except (ConnectionError, MPFlashError):
208
+ 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
+ #############################################################