mpflash 1.25.0.post1__py3-none-any.whl → 1.25.0.post3__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.
Files changed (52) hide show
  1. mpflash/add_firmware.py +43 -16
  2. mpflash/ask_input.py +4 -4
  3. mpflash/basicgit.py +1 -1
  4. mpflash/bootloader/manual.py +1 -1
  5. mpflash/cli_download.py +8 -5
  6. mpflash/cli_flash.py +31 -35
  7. mpflash/cli_group.py +3 -0
  8. mpflash/cli_list.py +8 -3
  9. mpflash/cli_main.py +4 -0
  10. mpflash/common.py +2 -38
  11. mpflash/config.py +21 -0
  12. mpflash/db/__init__.py +2 -0
  13. mpflash/db/core.py +61 -0
  14. mpflash/db/gather_boards.py +112 -0
  15. mpflash/db/loader.py +122 -0
  16. mpflash/db/meta.py +78 -0
  17. mpflash/db/micropython_boards.zip +0 -0
  18. mpflash/db/models.py +93 -0
  19. mpflash/db/tools.py +27 -0
  20. mpflash/download/__init__.py +46 -64
  21. mpflash/download/from_web.py +26 -36
  22. mpflash/download/fwinfo.py +41 -0
  23. mpflash/download/jid.py +56 -0
  24. mpflash/downloaded.py +79 -93
  25. mpflash/flash/__init__.py +7 -3
  26. mpflash/flash/esp.py +2 -1
  27. mpflash/flash/stm32.py +1 -1
  28. mpflash/flash/uf2/windows.py +3 -1
  29. mpflash/flash/worklist.py +16 -28
  30. mpflash/list.py +3 -3
  31. mpflash/logger.py +43 -9
  32. mpflash/mpboard_id/__init__.py +3 -9
  33. mpflash/mpboard_id/alternate.py +56 -0
  34. mpflash/mpboard_id/board_id.py +11 -94
  35. mpflash/mpboard_id/known.py +45 -57
  36. mpflash/mpboard_id/resolve.py +19 -0
  37. mpflash/mpremoteboard/__init__.py +3 -2
  38. mpflash/mpremoteboard/mpy_fw_info.py +1 -0
  39. mpflash/mpremoteboard/runner.py +5 -2
  40. mpflash/vendor/pydfu.py +33 -6
  41. mpflash/versions.py +3 -0
  42. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0.post3.dist-info}/METADATA +56 -12
  43. mpflash-1.25.0.post3.dist-info/RECORD +69 -0
  44. mpflash/db/boards.py +0 -63
  45. mpflash/db/downloads.py +0 -87
  46. mpflash/mpboard_id/add_boards.py +0 -260
  47. mpflash/mpboard_id/board.py +0 -40
  48. mpflash/mpboard_id/store.py +0 -47
  49. mpflash-1.25.0.post1.dist-info/RECORD +0 -62
  50. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0.post3.dist-info}/LICENSE +0 -0
  51. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0.post3.dist-info}/WHEEL +0 -0
  52. {mpflash-1.25.0.post1.dist-info → mpflash-1.25.0.post3.dist-info}/entry_points.txt +0 -0
mpflash/list.py CHANGED
@@ -16,7 +16,7 @@ def show_mcus(
16
16
  refresh: bool = True,
17
17
  *,
18
18
  title_style="magenta",
19
- header_style="bold magenta",
19
+ header_style="bold magenta",
20
20
  ):
21
21
  console.print(mcu_table(conn_mcus, title, refresh, title_style=title_style, header_style=header_style))
22
22
 
@@ -38,7 +38,7 @@ def mcu_table(
38
38
  ):
39
39
  """
40
40
  builds a rich table with the connected boards information
41
- The columns of the table are adjusted to the terminal width > 90
41
+ The columns of the table are adjusted to the terminal width > 90
42
42
  the columns are :
43
43
  Narrow Wide
44
44
  - Serial Yes Yes
@@ -83,7 +83,7 @@ def mcu_table(
83
83
  table.add_column("Port")
84
84
  table.add_column("Board", overflow="fold")
85
85
  if needs_variant:
86
- table.add_column("Variant")
86
+ table.add_column("Variant")
87
87
  if is_wide:
88
88
  table.add_column("CPU")
89
89
  table.add_column("Version", overflow="fold", min_width=5, max_width=16)
mpflash/logger.py CHANGED
@@ -1,4 +1,12 @@
1
- """Logging."""
1
+ """
2
+ Logger setup for CLI tools with Unicode-safe output.
3
+
4
+ Ensures log messages are compatible with the current console encoding.
5
+ Removes or replaces Unicode icons if the encoding is not UTF-8.
6
+ """
7
+
8
+ import functools
9
+ import sys
2
10
 
3
11
  from loguru import logger as log
4
12
  from rich.console import Console
@@ -7,12 +15,26 @@ from .config import config
7
15
 
8
16
  console = Console()
9
17
 
18
+ # Detect if the output encoding supports Unicode (UTF-8)
19
+ @functools.lru_cache(maxsize=1)
20
+ def _is_utf8_encoding() -> bool:
21
+ try:
22
+ encoding = getattr(sys.stdout, "encoding", None)
23
+ if encoding is None:
24
+ return False
25
+ return encoding.lower().replace("-", "") == "utf8"
26
+ except BaseException:
27
+ return False
10
28
 
11
29
  def _log_formatter(record: dict) -> str:
12
- """Log message formatter to combine loguru and rich formatting."""
30
+ """
31
+ Log message formatter for loguru and rich.
32
+
33
+ Removes Unicode icons if console encoding is not UTF-8.
34
+ """
13
35
  color_map = {
14
- "TRACE": "dim blue",
15
- "DEBUG": "cyan",
36
+ "TRACE": "cyan",
37
+ "DEBUG": "orange",
16
38
  "INFO": "bold",
17
39
  "SUCCESS": "bold green",
18
40
  "WARNING": "yellow",
@@ -20,11 +42,21 @@ def _log_formatter(record: dict) -> str:
20
42
  "CRITICAL": "bold white on red",
21
43
  }
22
44
  lvl_color = color_map.get(record["level"].name, "cyan")
23
- return "[not bold green]{time:HH:mm:ss}[/not bold green] | {level.icon} " + f"[{lvl_color}]{{message}}[/{lvl_color}]"
45
+ # Remove icon if not UTF-8
46
+ if _is_utf8_encoding():
47
+ icon = record["level"].icon
48
+ else:
49
+ icon = record["level"].name # fallback to text
50
+ # Insert color directly using f-string
51
+ return f"[not bold green]{{time:HH:mm:ss}}[/not bold green] | {icon} [{lvl_color}]{record['message']}[/{lvl_color}]"
52
+
24
53
 
54
+ def set_loglevel(loglevel: str) -> None:
55
+ """
56
+ Set the log level for the logger.
25
57
 
26
- def set_loglevel(loglevel: str):
27
- """Set the log level for the logger"""
58
+ Ensures Unicode safety for log output.
59
+ """
28
60
  try:
29
61
  log.remove()
30
62
  except ValueError:
@@ -32,8 +64,10 @@ def set_loglevel(loglevel: str):
32
64
  log.add(console.print, level=loglevel.upper(), colorize=False, format=_log_formatter) # type: ignore
33
65
 
34
66
 
35
- def make_quiet():
36
- """Make the logger quiet"""
67
+ def make_quiet() -> None:
68
+ """
69
+ Make the logger quiet.
70
+ """
37
71
  config.quiet = True
38
72
  console.quiet = True
39
73
  set_loglevel("CRITICAL")
@@ -3,15 +3,9 @@ Access to the micropython port and board information that is stored in the board
3
3
  that is included in the module.
4
4
 
5
5
  """
6
-
7
- from functools import lru_cache
8
- from typing import List, Optional, Tuple
9
- import importlib
10
6
  from mpflash.errors import MPFlashError
11
- from .board import Board
12
-
13
7
  from mpflash.versions import clean_version
14
- from .store import read_known_boardinfo
15
- from .known import get_known_ports, get_known_boards_for_port
16
- from .known import known_stored_boards, find_known_board
17
8
 
9
+ from .known import (find_known_board, get_known_boards_for_port, known_ports,
10
+ known_stored_boards)
11
+ from .resolve import resolve_board_ids
@@ -0,0 +1,56 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from loguru import logger as log
4
+
5
+
6
+ def alternate_board_names(board_id, port="") -> List[str]:
7
+ more = [board_id]
8
+
9
+ log.debug("try for renamed board_id")
10
+
11
+ if board_id.startswith("PICO"):
12
+ more.append(board_id.replace("PICO", "RPI_PICO"))
13
+ elif board_id.startswith("RPI_"):
14
+ more.append(board_id.replace("RPI_", ""))
15
+ elif board_id.startswith("GENERIC"):
16
+ if port:
17
+ more.append(board_id.replace("GENERIC", f"{port.upper()}_GENERIC"))
18
+ else:
19
+ # just add both of them
20
+ more.append(board_id.replace("GENERIC", f"ESP32_GENERIC"))
21
+ more.append(board_id.replace("GENERIC", f"ESP8266_GENERIC"))
22
+ elif board_id.startswith("ESP32_"):
23
+ more.append(board_id.replace("ESP32_", ""))
24
+ elif board_id.startswith("ESP8266_"):
25
+ more.append(board_id.replace("ESP8266_", ""))
26
+
27
+ # VARIANT
28
+ variant_suffixes = ["SPIRAM", "THREAD"]
29
+ for board in more:
30
+ if any(suffix in board for suffix in variant_suffixes):
31
+ for suffix in variant_suffixes:
32
+ if board.endswith(f"_{suffix}"):
33
+ more.append(board.replace(f"_{suffix}", ""))
34
+ # more.append(board.replace(f"_{suffix}", f"-{suffix}"))
35
+ break # first one found
36
+
37
+ return more
38
+
39
+
40
+ def add_renamed_boards(boards: List[str]) -> List[str]:
41
+ """
42
+ Adds the renamed boards to the list of boards.
43
+
44
+ Args:
45
+ boards : The list of boards to add the renamed boards to.
46
+
47
+ Returns:
48
+ List[str]: The list of boards with the renamed boards added.
49
+ """
50
+
51
+ _boards = boards.copy()
52
+ for board in boards:
53
+ _boards.extend(alternate_board_names(board))
54
+ if board != board.upper():
55
+ _boards.extend(alternate_board_names(board.upper()))
56
+ return _boards
@@ -2,18 +2,14 @@
2
2
  Translate board description to board designator
3
3
  """
4
4
 
5
- import functools
6
- import re
7
- import sqlite3
8
- from pathlib import Path
5
+
9
6
  from typing import List, Optional
10
7
 
11
- from mpflash.config import config
8
+ from mpflash.db.core import Session
9
+ from mpflash.db.models import Board
12
10
  from mpflash.errors import MPFlashError
13
11
  from mpflash.logger import log
14
- from mpflash.mpboard_id.board import Board
15
- from mpflash.mpboard_id.store import read_known_boardinfo
16
- from mpflash.versions import clean_version, get_preview_mp_version, get_stable_mp_version
12
+ from mpflash.versions import clean_version
17
13
 
18
14
 
19
15
  def find_board_id_by_description(
@@ -21,7 +17,6 @@ def find_board_id_by_description(
21
17
  short_descr: str,
22
18
  *,
23
19
  version: str,
24
- board_info: Optional[Path] = None,
25
20
  ) -> Optional[str]:
26
21
  """Find the MicroPython BOARD_ID based on the description in the firmware"""
27
22
  version = clean_version(version) if version else ""
@@ -29,7 +24,6 @@ def find_board_id_by_description(
29
24
  boards = _find_board_id_by_description(
30
25
  descr=descr,
31
26
  short_descr=short_descr,
32
- db_path=board_info,
33
27
  version=version,
34
28
  )
35
29
  if not boards:
@@ -37,7 +31,6 @@ def find_board_id_by_description(
37
31
  boards = _find_board_id_by_description(
38
32
  descr=descr,
39
33
  short_descr=short_descr,
40
- db_path=board_info,
41
34
  version="%", # any version
42
35
  )
43
36
  return boards[0].board_id if boards else None
@@ -51,7 +44,6 @@ def _find_board_id_by_description(
51
44
  short_descr: str,
52
45
  version: Optional[str] = None,
53
46
  variant: str = "",
54
- db_path: Optional[Path] = None,
55
47
  ):
56
48
  short_descr = short_descr or ""
57
49
  boards: List[Board] = []
@@ -63,90 +55,15 @@ def _find_board_id_by_description(
63
55
  descriptions.append(descr[8:])
64
56
  descriptions.append(short_descr[8:])
65
57
 
66
- try:
67
- with sqlite3.connect(db_path or config.db_path) as conn:
68
- conn.row_factory = sqlite3.Row
69
- cursor = conn.cursor()
70
-
71
- qry = f"""
72
- SELECT
73
- *
74
- FROM board_downloaded
75
- WHERE
76
- board_id IN (
77
- SELECT DISTINCT board_id
78
- FROM board_downloaded
79
- WHERE description IN {str(tuple(descriptions))}
80
- )
81
- AND version like '{version}'
82
- AND variant like '{variant}'
83
- """
84
- cursor.execute(qry)
85
- rows = cursor.fetchall()
86
- for row in rows:
87
- r = dict(row)
58
+ with Session() as session:
59
+ qry = session.query(Board).filter(
60
+ Board.description.in_(descriptions),
61
+ Board.version.like(version),
62
+ Board.variant.like(variant),
63
+ )
64
+ boards = qry.all()
88
65
 
89
- boards.append(Board.from_dict(dict(row)))
90
- except sqlite3.OperationalError as e:
91
- raise MPFlashError("Database error") from e
92
66
  if not boards:
93
67
  raise MPFlashError(f"No board info found for description '{descr}' or '{short_descr}'")
94
68
  return boards
95
69
 
96
-
97
- @functools.lru_cache(maxsize=20)
98
- def _find_board_id_by_description_xx(
99
- *,
100
- descr: str,
101
- short_descr: str,
102
- version: Optional[str] = None,
103
- board_info: Optional[Path] = None,
104
- ):
105
- """
106
- Find the MicroPython BOARD_ID based on the description in the firmware
107
- using the pre-built board_info.json file
108
-
109
- Parameters:
110
- descr: str
111
- Description of the board
112
- short_descr: str
113
- Short description of the board (optional)
114
- version: str
115
- Version of the MicroPython firmware
116
- board_info: Path
117
- Path to the board_info.json file (optional)
118
-
119
- """
120
- # Some functional overlap with
121
- # src\mpflash\mpflash\mpboard_id\__init__.py find_known_board
122
-
123
- candidate_boards = read_known_boardinfo(board_info)
124
- if not short_descr and " with " in descr:
125
- short_descr = descr.split(" with ")[0]
126
- if version:
127
- # filter for matching version
128
- if version in ("stable"):
129
- version = get_stable_mp_version()
130
- if version in ("preview", "master"):
131
- version = get_preview_mp_version()
132
- known_versions = sorted({b.version for b in candidate_boards})
133
- if version not in known_versions:
134
- log.trace(known_versions)
135
- log.debug(f"Version {version} not found in board info, using latest stable version {get_stable_mp_version()}")
136
- version = ".".join(get_stable_mp_version().split(".")[:2]) # take only major.minor
137
- if version_matches := [b for b in candidate_boards if b.version.startswith(version)]:
138
- candidate_boards = version_matches
139
- else:
140
- raise MPFlashError(f"No board info found for version {version}")
141
- # First try full match on description, then partial match
142
- matches = [b for b in candidate_boards if b.description == descr]
143
- if not matches and short_descr:
144
- matches = [b for b in candidate_boards if b.description == short_descr]
145
- if not matches:
146
- # partial match (for added VARIANT)
147
- matches = [b for b in candidate_boards if b.description.startswith(descr)]
148
- if not matches and short_descr:
149
- matches = [b for b in candidate_boards if b.description.startswith(short_descr)]
150
- if not matches:
151
- raise MPFlashError(f"No board info found for description '{descr}' or '{short_descr}'")
152
- return sorted(matches, key=lambda x: x.version)
@@ -7,62 +7,50 @@ This module provides access to the board info and the known ports and boards."""
7
7
  from functools import lru_cache
8
8
  from typing import List, Optional, Tuple
9
9
 
10
- from mpflash.db.boards import find_board_id, find_board_info
10
+ from sqlalchemy import text
11
+
12
+ from mpflash.db.core import Session
13
+ from mpflash.db.models import Board
11
14
  from mpflash.errors import MPFlashError
12
- from mpflash.versions import clean_version
13
15
  from mpflash.logger import log
16
+ from mpflash.versions import clean_version
14
17
 
15
- from .board import Board
16
- from .store import read_known_boardinfo
17
18
 
19
+ def known_ports(version: str = "") -> list[str]:
20
+ """Return a list of known ports for a given version."""
21
+ version = clean_version(version) if version else "%%"
22
+ with Session() as session:
23
+ qry = text("SELECT distinct port FROM boards WHERE version like :version ORDER BY port;")
24
+ ports = session.execute(qry, {"version": version}).columns("port").fetchall()
25
+ return [row.port for row in ports]
18
26
 
19
- def get_known_ports() -> List[str]:
20
- # TODO: Filter for Version
21
- log.warning("get_known_ports() is deprecated")
22
- mp_boards = read_known_boardinfo()
23
- # select the unique ports from info
24
- ports = set({board.port for board in mp_boards if board.port})
25
- return sorted(list(ports))
26
27
 
28
+ def known_versions(port: str = "") -> list[str]:
29
+ """Return a list of known versions for a given port."""
30
+ port = port.strip() if port else "%%"
31
+ with Session() as session:
32
+ qry = text("SELECT distinct version FROM boards WHERE port like :port ORDER BY version;")
33
+ versions = session.execute(qry, {"port": port}).columns("version").fetchall()
34
+ return [row.version for row in versions]
27
35
 
28
- def get_known_boards_for_port(port: Optional[str] = "", versions: Optional[List[str]] = None) -> List[Board]:
36
+
37
+ def get_known_boards_for_port(port: str = "", versions: List[str] = []):
29
38
  """
30
39
  Returns a list of boards for the given port and version(s)
31
40
 
32
41
  port: The Micropython port to filter for
33
42
  versions: Optional, The Micropython versions to filter for (actual versions required)
34
43
  """
35
- mp_boards = read_known_boardinfo()
36
- if versions:
37
- preview_or_stable = "preview" in versions or "stable" in versions
38
- else:
39
- preview_or_stable = False
40
-
41
- # filter for 'preview' as they are not in the board_info.json
42
- # instead use stable version
43
- versions = versions or []
44
- if "preview" in versions:
45
- versions.remove("preview")
46
- versions.append("stable")
47
- # filter for the port
48
- if port:
49
- mp_boards = [board for board in mp_boards if board.port == port]
50
- if versions:
51
- # make sure of the v prefix
52
- versions = [clean_version(v) for v in versions]
53
- # filter for the version(s)
54
- mp_boards = [board for board in mp_boards if board.version in versions]
55
- if not mp_boards and preview_or_stable:
56
- # nothing found - perhaps there is a newer version for which we do not have the board info yet
57
- # use the latest known version from the board info
58
- mp_boards = read_known_boardinfo()
59
- last_known_version = sorted({b.version for b in mp_boards})[-1]
60
- mp_boards = [board for board in mp_boards if board.version == last_known_version]
61
-
62
- return mp_boards
63
-
64
-
65
- def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List[Tuple[str, str]]:
44
+ versions = [clean_version(v) for v in versions] if versions else []
45
+ with Session() as session:
46
+ qry = session.query(Board).filter(Board.port.like(port))
47
+ if versions:
48
+ qry = qry.filter(Board.version.in_(versions))
49
+ boards = qry.all()
50
+ return boards
51
+
52
+
53
+ def known_stored_boards(port: str, versions: List[str] = []) -> List[Tuple[str, str]]:
66
54
  """
67
55
  Returns a list of tuples with the description and board name for the given port and version
68
56
 
@@ -71,22 +59,22 @@ def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List
71
59
  """
72
60
  mp_boards = get_known_boards_for_port(port, versions)
73
61
 
74
- boards = set({(f"{board.version} {board.description}", board.board_id) for board in mp_boards})
62
+ boards = set({(f"{board.version} {board.board_id:<30} {board.description}", board.board_id) for board in mp_boards})
75
63
  return sorted(list(boards))
76
64
 
77
65
 
78
- @lru_cache(maxsize=20)
79
- def find_known_board(board_id: str, version ="") -> Board:
66
+ def find_known_board(board_id: str, version="") -> Board:
80
67
  """Find the board for the given BOARD_ID or 'board description' and return the board info as a Board object"""
81
- # Some functional overlap with:
82
- # mpboard_id\board_id.py _find_board_id_by_description
83
- # TODO: Refactor to search the SQLite DB instead of the JSON file
84
- board_ids = find_board_id(board_id = board_id, version = version or "%")
85
- boards = []
86
- for board_id in board_ids:
87
- # if we have a board_id, use it to find the board info
88
- boards += [Board.from_dict(dict(r)) for r in find_board_info(board_id = board_id)]
89
-
90
- if boards:
91
- return boards[0]
68
+ with Session() as session:
69
+ qry = session.query(Board).filter(Board.board_id == board_id)
70
+ if version:
71
+ qry = qry.filter(Board.version == version)
72
+ board = qry.first()
73
+ if not board:
74
+ # if no board found, try to find it by description
75
+ qry = session.query(Board).filter(Board.description == board_id)
76
+ if version:
77
+ qry = qry.filter(Board.version == version)
78
+ if board:
79
+ return board
92
80
  raise MPFlashError(f"Board {board_id} not found")
@@ -0,0 +1,19 @@
1
+ from logging import log
2
+ from mpflash.common import Params
3
+ from mpflash.mpboard_id import find_known_board
4
+
5
+
6
+ def resolve_board_ids(params: Params):
7
+ """Resolve board descriptions to board_id, and remove empty strings from list of boards"""
8
+ for board_id in params.boards:
9
+ if board_id == "":
10
+ params.boards.remove(board_id)
11
+ continue
12
+ if " " in board_id:
13
+ try:
14
+ if info := find_known_board(board_id):
15
+ log.info(f"Resolved board description: {info.board_id}")
16
+ params.boards.remove(board_id)
17
+ params.boards.append(info.board_id)
18
+ except Exception as e:
19
+ log.warning(f"Unable to resolve board description: {e}")
@@ -170,7 +170,7 @@ class MPRemoteBoard:
170
170
  self.cpu = info["cpu"]
171
171
  self.arch = info["arch"]
172
172
  self.mpy = info["mpy"]
173
- self.description = descr = info["board"]
173
+ self.description = descr = info["description"] if 'description' in info else info["board"]
174
174
  pos = descr.rfind(" with")
175
175
  short_descr = descr[:pos].strip() if pos != -1 else ""
176
176
  if info.get("board_id", None):
@@ -216,9 +216,10 @@ class MPRemoteBoard:
216
216
  self.toml = {}
217
217
  if rc in [OK]: # sometimes we get an -9 ???
218
218
  try:
219
+ log.trace(result)
219
220
  # Ok we have the info, now parse it
220
221
  self.toml = tomllib.loads("".join(result))
221
- log.debug(f"board_info.toml: {self.toml}")
222
+ log.debug("board_info.toml:" + self.toml["description"])
222
223
  except Exception as e:
223
224
  log.error(f"Failed to parse board_info.toml: {e}")
224
225
  else:
@@ -51,6 +51,7 @@ def _info(): # type:() -> dict[str, str]
51
51
  try:
52
52
  _machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
53
53
  info["board"] = _machine.strip()
54
+ info["description"] = _machine.strip()
54
55
  si_build = sys.implementation._build if "_build" in dir(sys.implementation) else ""
55
56
  if si_build:
56
57
  info["board"] = si_build.split("-")[0]
@@ -91,11 +91,12 @@ def run(
91
91
  )
92
92
  except FileNotFoundError as e:
93
93
  raise FileNotFoundError(f"Failed to start {cmd[0]}") from e
94
-
94
+ _timed_out = False
95
95
  def timed_out():
96
- proc.kill()
96
+ _timed_out = True
97
97
  if log_warnings:
98
98
  log.warning(f"Command {cmd} timed out after {timeout} seconds")
99
+ proc.kill()
99
100
 
100
101
  timer = Timer(timeout, timed_out)
101
102
  try:
@@ -135,6 +136,8 @@ def run(
135
136
  log.error(f"Failed to decode output: {e}")
136
137
  finally:
137
138
  timer.cancel()
139
+ if _timed_out:
140
+ raise TimeoutError(f"Command {cmd} timed out after {timeout} seconds")
138
141
 
139
142
  proc.wait(timeout=1)
140
143
  return proc.returncode or 0, output
mpflash/vendor/pydfu.py CHANGED
@@ -338,7 +338,7 @@ def read_dfu_file(filename):
338
338
  # I uint32_t size Size of the DFU file (without suffix)
339
339
  # B uint8_t targets Number of targets
340
340
  dfu_prefix, data = consume("<5sBIB", data, "signature version size targets")
341
- print(" %(signature)s v%(version)d, image size: %(size)d, " "targets: %(targets)d" % dfu_prefix)
341
+ print(" %(signature)s v%(version)d, image size: %(size)d, targets: %(targets)d" % dfu_prefix)
342
342
  for target_idx in range(dfu_prefix["targets"]):
343
343
  # Decode the Image Prefix
344
344
  #
@@ -350,15 +350,14 @@ def read_dfu_file(filename):
350
350
  # 255s char[255] name Name of the target
351
351
  # I uint32_t size Size of image (without prefix)
352
352
  # I uint32_t elements Number of elements in the image
353
- img_prefix, data = consume("<6sBI255s2I", data, "signature altsetting named name " "size elements")
353
+ img_prefix, data = consume("<6sBI255s2I", data, "signature altsetting named name size elements")
354
354
  img_prefix["num"] = target_idx
355
355
  if img_prefix["named"]:
356
356
  img_prefix["name"] = cstring(img_prefix["name"])
357
357
  else:
358
358
  img_prefix["name"] = ""
359
359
  print(
360
- " %(signature)s %(num)d, alt setting: %(altsetting)s, "
361
- 'name: "%(name)s", size: %(size)d, elements: %(elements)d' % img_prefix
360
+ ' %(signature)s %(num)d, alt setting: %(altsetting)s, name: "%(name)s", size: %(size)d, elements: %(elements)d' % img_prefix
362
361
  )
363
362
 
364
363
  target_size = img_prefix["size"]
@@ -395,7 +394,7 @@ def read_dfu_file(filename):
395
394
  # B uint8_t len 16
396
395
  # I uint32_t crc32 Checksum
397
396
  dfu_suffix = named(struct.unpack("<4H3sBI", data[:16]), "device product vendor dfu ufd len crc")
398
- print(" usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, " "dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x" % dfu_suffix)
397
+ print(" usb: %(vendor)04x:%(product)04x, device: 0x%(device)04x, dfu: 0x%(dfu)04x, %(ufd)s, %(len)d, 0x%(crc)08x" % dfu_suffix)
399
398
  if crc != dfu_suffix["crc"]:
400
399
  print("CRC ERROR: computed crc32 is 0x%08x" % crc)
401
400
  return
@@ -407,6 +406,20 @@ def read_dfu_file(filename):
407
406
  return elements
408
407
 
409
408
 
409
+ def read_bin_file(filename, address):
410
+ """Reads binary file(.bin) and stores it as single
411
+ element in element array just like read_dfu_file() would.
412
+ """
413
+ element = {}
414
+ print("File: {}".format(filename))
415
+ with open(filename, "rb") as fin:
416
+ element["data"] = fin.read()
417
+ element["size"] = len(element["data"])
418
+ element["num"] = 0
419
+ element["addr"] = address
420
+ return [element]
421
+
422
+
410
423
  class FilterDFU(object):
411
424
  """Class for filtering USB devices to identify devices which are in DFU
412
425
  mode.
@@ -542,6 +555,13 @@ def main():
542
555
  parser.add_argument("--pid", help="USB Product ID", type=lambda x: int(x, 0), default=None)
543
556
  parser.add_argument("-m", "--mass-erase", help="mass erase device", action="store_true", default=False)
544
557
  parser.add_argument("-u", "--upload", help="read file from DFU device", dest="path", default=False)
558
+ parser.add_argument(
559
+ "-a",
560
+ "--address",
561
+ help="specify target memory address(hex or dec) when uploading .bin files",
562
+ type=lambda x: int(x, 0),
563
+ default=None,
564
+ )
545
565
  parser.add_argument("-x", "--exit", help="Exit DFU", action="store_true", default=False)
546
566
  parser.add_argument("-v", "--verbose", help="increase output verbosity", action="store_true", default=False)
547
567
  args = parser.parse_args()
@@ -568,7 +588,14 @@ def main():
568
588
  command_run = True
569
589
 
570
590
  if args.path:
571
- elements = read_dfu_file(args.path)
591
+ if str(args.path).endswith(".bin"):
592
+ if args.address is None:
593
+ raise ValueError("Address must be specified using -a when uploading binary")
594
+
595
+ elements = read_bin_file(args.path, args.addr)
596
+ else:
597
+ elements = read_dfu_file(args.path)
598
+
572
599
  if not elements:
573
600
  print("No data in dfu file")
574
601
  return
mpflash/versions.py CHANGED
@@ -78,6 +78,7 @@ def is_version(version: str):
78
78
  """Check if the version is a valid version string"""
79
79
  # Just in time import
80
80
  from packaging.version import Version
81
+
81
82
  return Version._regex.search(version) is not None
82
83
 
83
84
 
@@ -86,6 +87,7 @@ def micropython_versions(minver: str = "v1.20", reverse: bool = False, cache_it=
86
87
  """Get the list of micropython versions from github tags"""
87
88
  # Just in time import
88
89
  from packaging.version import parse
90
+
89
91
  try:
90
92
  gh_client = config.gh_client
91
93
  repo = gh_client.get_repo("micropython/micropython")
@@ -126,6 +128,7 @@ def checkedout_version(path: Path, flat: bool = False) -> str:
126
128
  """Get the checked-out version of the repo"""
127
129
  # Just in time import
128
130
  import mpflash.basicgit as git
131
+
129
132
  version = git.get_local_tag(path.as_posix())
130
133
  if not version:
131
134
  raise ValueError("No valid Tag found")