mpflash 1.24.8__py3-none-any.whl → 1.25.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/downloaded.py CHANGED
@@ -5,27 +5,12 @@ import jsonlines
5
5
  from loguru import logger as log
6
6
 
7
7
  from mpflash.common import PORT_FWTYPES, FWInfo
8
+ from mpflash.db.downloads import downloaded
8
9
  from mpflash.versions import clean_version
9
10
 
10
11
  from .config import config
11
12
 
12
-
13
13
  # #########################################################################################################
14
- def downloaded_firmwares(fw_folder: Path) -> List[FWInfo]:
15
- """Load a list of locally downloaded firmwares from the jsonl file"""
16
- firmwares: List[FWInfo] = []
17
- log.debug(f"Reading {fw_folder / 'firmware.jsonl' }")
18
- try:
19
- with jsonlines.open(fw_folder / "firmware.jsonl") as reader:
20
- firmwares = [FWInfo.from_dict(item) for item in reader]
21
- except FileNotFoundError:
22
- log.error(f"No firmware.jsonl found in {fw_folder}")
23
- except jsonlines.InvalidLineError as e:
24
- log.error(f"Invalid firmware.jsonl found in {fw_folder} : {e}")
25
-
26
- # sort by filename
27
- firmwares.sort(key=lambda x: x.filename)
28
- return firmwares
29
14
 
30
15
 
31
16
  def clean_downloaded_firmwares(fw_folder: Path) -> None:
@@ -33,15 +18,9 @@ def clean_downloaded_firmwares(fw_folder: Path) -> None:
33
18
  Remove duplicate entries from the firmware.jsonl file, keeping the latest one
34
19
  uniqueness is based on the filename
35
20
  """
36
- firmwares = downloaded_firmwares(fw_folder)
37
- if not firmwares:
38
- return
39
- # keep the latest entry
40
- unique_fw = {fw.filename: fw for fw in firmwares}
41
- with jsonlines.open(fw_folder / "firmware.jsonl", "w") as writer:
42
- for fw in unique_fw.values():
43
- writer.write(fw.to_dict())
44
- log.info(f"Removed duplicate entries from firmware.jsonl in {fw_folder}")
21
+ # Duplication should no longer happen,
22
+ # but is would be a good idea to check the consistence between the DB and the downloads folder sometimes
23
+ pass
45
24
 
46
25
 
47
26
  def find_downloaded_firmware(
@@ -50,16 +29,16 @@ def find_downloaded_firmware(
50
29
  version: str = "", # v1.2.3
51
30
  port: str = "",
52
31
  variants: bool = False,
53
- fw_folder: Optional[Path] = None,
32
+ db_path: Optional[Path] = None,
54
33
  trie: int = 1,
55
34
  selector: Optional[Dict[str, str]] = None,
56
35
  ) -> List[FWInfo]:
57
36
  if selector is None:
58
37
  selector = {}
59
- fw_folder = fw_folder or config.firmware_folder
38
+
60
39
  # Use the information in firmwares.jsonl to find the firmware file
61
40
  log.debug(f"{trie}] Looking for firmware for {board_id} {version} ")
62
- fw_list = downloaded_firmwares(fw_folder)
41
+ fw_list = downloaded()
63
42
  if not fw_list:
64
43
  log.error("No firmware files found. Please download the firmware first.")
65
44
  return []
@@ -80,7 +59,7 @@ def find_downloaded_firmware(
80
59
  board_id = board_id.replace("_", "-")
81
60
 
82
61
  fw_list = find_downloaded_firmware(
83
- fw_folder=fw_folder,
62
+ db_path=db_path,
84
63
  board_id=board_id,
85
64
  version=version,
86
65
  port=port,
mpflash/flash/worklist.py CHANGED
@@ -43,7 +43,7 @@ def auto_update(
43
43
  log.warning(f"Skipping flashing {mcu.family} {mcu.port} {mcu.board} on {mcu.serialport} as it is not a MicroPython firmware")
44
44
  continue
45
45
  board_firmwares = find_downloaded_firmware(
46
- fw_folder=fw_folder,
46
+ db_path=fw_folder,
47
47
  board_id=mcu.board if not mcu.variant else f"{mcu.board}-{mcu.variant}",
48
48
  version=target_version,
49
49
  port=mcu.port,
@@ -91,11 +91,11 @@ def manual_worklist(
91
91
  # need the CPU type for the esptool
92
92
  mcu.cpu = info.cpu
93
93
  except (LookupError, MPFlashError) as e:
94
- log.error(f"Board {board_id} not found in board_info.zip")
94
+ log.error(f"Board {board_id} not found in board database")
95
95
  log.exception(e)
96
96
  return []
97
97
  mcu.board = board_id
98
- firmwares = find_downloaded_firmware(fw_folder=fw_folder, board_id=board_id, version=version, port=mcu.port)
98
+ firmwares = find_downloaded_firmware(db_path=fw_folder, board_id=board_id, version=version, port=mcu.port)
99
99
  if not firmwares:
100
100
  log.error(f"No firmware found for {mcu.port} {board_id} version {version}")
101
101
  return []
@@ -21,17 +21,20 @@ class Board:
21
21
  mcu_name: str = field(default="")
22
22
  cpu: str = field(default="")
23
23
  variant: str = field(default="")
24
+ mcu: str = field(default="")
24
25
 
25
26
  def __post_init__(self):
26
27
  if not self.cpu:
27
28
  if " with " in self.description:
28
- self.cpu = self.description.split(" with ")[-1]
29
+ self.cpu = self.description.rsplit(" with ")[-1]
29
30
  else:
30
31
  self.cpu = self.port
31
32
 
32
33
  @staticmethod
33
34
  def from_dict(data: dict) -> "Board":
34
- return Board(**data)
35
+ valid_keys = {field.name for field in Board.__dataclass_fields__.values()}
36
+ filtered_data = {k: v for k, v in data.items() if k in valid_keys}
37
+ return Board(**filtered_data)
35
38
 
36
39
  def to_dict(self) -> dict:
37
40
  return self.__dict__
@@ -3,11 +3,15 @@ Translate board description to board designator
3
3
  """
4
4
 
5
5
  import functools
6
+ import re
7
+ import sqlite3
6
8
  from pathlib import Path
7
- from typing import Optional
9
+ from typing import List, Optional
8
10
 
11
+ from mpflash.config import config
9
12
  from mpflash.errors import MPFlashError
10
13
  from mpflash.logger import log
14
+ from mpflash.mpboard_id.board import Board
11
15
  from mpflash.mpboard_id.store import read_known_boardinfo
12
16
  from mpflash.versions import clean_version, get_preview_mp_version, get_stable_mp_version
13
17
 
@@ -20,21 +24,78 @@ def find_board_id_by_description(
20
24
  board_info: Optional[Path] = None,
21
25
  ) -> Optional[str]:
22
26
  """Find the MicroPython BOARD_ID based on the description in the firmware"""
23
-
27
+ version = clean_version(version) if version else ""
24
28
  try:
25
29
  boards = _find_board_id_by_description(
26
30
  descr=descr,
27
31
  short_descr=short_descr,
28
- board_info=board_info,
29
- version=clean_version(version) if version else None,
32
+ db_path=board_info,
33
+ version=version,
30
34
  )
31
- return boards[-1].board_id
35
+ if not boards:
36
+ log.debug(f"Version {version} not found in board info, using any version")
37
+ boards = _find_board_id_by_description(
38
+ descr=descr,
39
+ short_descr=short_descr,
40
+ db_path=board_info,
41
+ version="%", # any version
42
+ )
43
+ return boards[0].board_id if boards else None
32
44
  except MPFlashError:
33
45
  return "UNKNOWN_BOARD"
34
46
 
35
47
 
36
- @functools.lru_cache(maxsize=20)
37
48
  def _find_board_id_by_description(
49
+ *,
50
+ descr: str,
51
+ short_descr: str,
52
+ version: Optional[str] = None,
53
+ variant: str = "",
54
+ db_path: Optional[Path] = None,
55
+ ):
56
+ short_descr = short_descr or ""
57
+ boards: List[Board] = []
58
+ version = clean_version(version) if version else "%"
59
+ if "-preview" in version:
60
+ version = version.replace("-preview", "%")
61
+ descriptions = [descr, short_descr]
62
+ if descr.startswith("Generic"):
63
+ descriptions.append(descr[8:])
64
+ descriptions.append(short_descr[8:])
65
+
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)
88
+
89
+ boards.append(Board.from_dict(dict(row)))
90
+ except sqlite3.OperationalError as e:
91
+ raise MPFlashError("Database error") from e
92
+ if not boards:
93
+ raise MPFlashError(f"No board info found for description '{descr}' or '{short_descr}'")
94
+ return boards
95
+
96
+
97
+ @functools.lru_cache(maxsize=20)
98
+ def _find_board_id_by_description_xx(
38
99
  *,
39
100
  descr: str,
40
101
  short_descr: str,
Binary file
@@ -7,8 +7,10 @@ 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
11
  from mpflash.errors import MPFlashError
11
12
  from mpflash.versions import clean_version
13
+ from mpflash.logger import log
12
14
 
13
15
  from .board import Board
14
16
  from .store import read_known_boardinfo
@@ -16,6 +18,7 @@ from .store import read_known_boardinfo
16
18
 
17
19
  def get_known_ports() -> List[str]:
18
20
  # TODO: Filter for Version
21
+ log.warning("get_known_ports() is deprecated")
19
22
  mp_boards = read_known_boardinfo()
20
23
  # select the unique ports from info
21
24
  ports = set({board.port for board in mp_boards if board.port})
@@ -41,6 +44,9 @@ def get_known_boards_for_port(port: Optional[str] = "", versions: Optional[List[
41
44
  if "preview" in versions:
42
45
  versions.remove("preview")
43
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]
44
50
  if versions:
45
51
  # make sure of the v prefix
46
52
  versions = [clean_version(v) for v in versions]
@@ -53,9 +59,6 @@ def get_known_boards_for_port(port: Optional[str] = "", versions: Optional[List[
53
59
  last_known_version = sorted({b.version for b in mp_boards})[-1]
54
60
  mp_boards = [board for board in mp_boards if board.version == last_known_version]
55
61
 
56
- # filter for the port
57
- if port:
58
- mp_boards = [board for board in mp_boards if board.port == port]
59
62
  return mp_boards
60
63
 
61
64
 
@@ -73,22 +76,33 @@ def known_stored_boards(port: str, versions: Optional[List[str]] = None) -> List
73
76
 
74
77
 
75
78
  @lru_cache(maxsize=20)
76
- def find_known_board(board_id: str) -> Board:
79
+ def find_known_board(board_id: str, version ="") -> Board:
77
80
  """Find the board for the given BOARD_ID or 'board description' and return the board info as a Board object"""
78
81
  # Some functional overlap with:
79
82
  # mpboard_id\board_id.py _find_board_id_by_description
80
- info = read_known_boardinfo()
81
- for board_info in info:
82
- if board_id in (
83
- board_info.board_id,
84
- board_info.description,
85
- ) or board_info.description.startswith(board_id):
86
- if not board_info.cpu:
87
- # failsafe for older board_info.json files
88
- print(f"Board {board_id} has no CPU info, using port as CPU")
89
- if " with " in board_info.description:
90
- board_info.cpu = board_info.description.split(" with ")[-1]
91
- else:
92
- board_info.cpu = board_info.port
93
- return board_info
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
+
91
+ # if board_ids:
92
+ # # if we have a board_id, use it to find the board info
93
+ # board_id = board_ids[0]
94
+ # info = read_known_boardinfo()
95
+ # for board_info in info:
96
+ # if board_id in (
97
+ # board_info.board_id,
98
+ # board_info.description,
99
+ # ) or board_info.description.startswith(board_id):
100
+ # if not board_info.cpu:
101
+ # # failsafe for older board_info.json files
102
+ # print(f"Board {board_id} has no CPU info, using port as CPU")
103
+ # if " with " in board_info.description:
104
+ # board_info.cpu = board_info.description.split(" with ")[-1]
105
+ # else:
106
+ # board_info.cpu = board_info.port
107
+ # return board_info
94
108
  raise MPFlashError(f"Board {board_id} not found")
@@ -2,6 +2,7 @@ import functools
2
2
  import zipfile
3
3
  from pathlib import Path
4
4
  from typing import Final, List, Optional
5
+ from mpflash.logger import log
5
6
 
6
7
  import jsons
7
8
 
@@ -32,7 +33,7 @@ def write_boardinfo_json(board_list: List[Board], *, folder: Optional[Path] = No
32
33
  @functools.lru_cache(maxsize=20)
33
34
  def read_known_boardinfo(board_info: Optional[Path] = None) -> List[Board]:
34
35
  """Reads the board information from a JSON file in a zip file."""
35
-
36
+ log.warning("read_known_boardinfo() is deprecated")
36
37
 
37
38
  if not board_info:
38
39
  board_info = HERE / "board_info.zip"
@@ -2,7 +2,6 @@
2
2
  Module to run mpremote commands, and retry on failure or timeout
3
3
  """
4
4
 
5
-
6
5
  import contextlib
7
6
  import sys
8
7
  import time
@@ -35,7 +34,9 @@ RETRIES = 3
35
34
  class MPRemoteBoard:
36
35
  """Class to run mpremote commands"""
37
36
 
38
- def __init__(self, serialport: str = "", update: bool = False, *, location: str = ""):
37
+ def __init__(
38
+ self, serialport: str = "", update: bool = False, *, location: str = ""
39
+ ):
39
40
  """
40
41
  Initialize MPRemoteBoard object.
41
42
 
@@ -43,6 +44,8 @@ class MPRemoteBoard:
43
44
  - serialport (str): The serial port to connect to. Default is an empty string.
44
45
  - update (bool): Whether to update the MCU information. Default is False.
45
46
  """
47
+ self._board_id = ""
48
+
46
49
  self.serialport: str = serialport
47
50
  self.firmware = {}
48
51
 
@@ -52,8 +55,6 @@ class MPRemoteBoard:
52
55
  self.description = ""
53
56
  self.version = ""
54
57
  self.port = ""
55
- self.board = ""
56
- self.variant= ""
57
58
  self.cpu = ""
58
59
  self.arch = ""
59
60
  self.mpy = ""
@@ -63,6 +64,33 @@ class MPRemoteBoard:
63
64
  if update:
64
65
  self.get_mcu_info()
65
66
 
67
+ ###################################
68
+ # board_id := board[-variant]
69
+ @property
70
+ def board_id(self) -> str:
71
+ return self._board_id
72
+
73
+ @board_id.setter
74
+ def board_id(self, value: str) -> None:
75
+ self._board_id = value.rstrip("-")
76
+
77
+ @property
78
+ def board(self) -> str:
79
+ return self._board_id.split("-")[0]
80
+
81
+ @board.setter
82
+ def board(self, value: str) -> None:
83
+ self.board_id = f"{value}-{self.variant}" if self.variant else value
84
+
85
+ @property
86
+ def variant(self) -> str:
87
+ return self._board_id.split("-")[1] if "-" in self._board_id else ""
88
+
89
+ @variant.setter
90
+ def variant(self, value: str) -> None:
91
+ self.board_id = f"{self.board}-{value}"
92
+
93
+ ###################################
66
94
  def __str__(self):
67
95
  """
68
96
  Return a string representation of the MPRemoteBoard object.
@@ -73,7 +101,9 @@ class MPRemoteBoard:
73
101
  return f"MPRemoteBoard({self.serialport}, {self.family} {self.port}, {self.board}{f'-{self.variant}' if self.variant else ''}, {self.version})"
74
102
 
75
103
  @staticmethod
76
- def connected_boards(bluetooth: bool = False, description: bool = False) -> List[str]:
104
+ def connected_boards(
105
+ bluetooth: bool = False, description: bool = False
106
+ ) -> List[str]:
77
107
  # TODO: rename to connected_comports
78
108
  """
79
109
  Get a list of connected comports.
@@ -101,7 +131,10 @@ class MPRemoteBoard:
101
131
  if sys.platform == "win32":
102
132
  # Windows sort of comports by number - but fallback to device name
103
133
  return sorted(
104
- output, key=lambda x: int(x.split()[0][3:]) if x.split()[0][3:].isdigit() else x
134
+ output,
135
+ key=lambda x: int(x.split()[0][3:])
136
+ if x.split()[0][3:].isdigit()
137
+ else x,
105
138
  )
106
139
  # sort by device name
107
140
  return sorted(output)
@@ -123,11 +156,11 @@ class MPRemoteBoard:
123
156
  timeout=timeout,
124
157
  resume=False, # Avoid restarts
125
158
  )
126
- if rc:
159
+ if rc not in (0, 1): ## WORKAROUND - SUDDEN RETURN OF 1 on success
127
160
  log.debug(f"rc: {rc}, result: {result}")
128
161
  raise ConnectionError(f"Failed to get mcu_info for {self.serialport}")
129
162
  # Ok we have the info, now parse it
130
- raw_info = result[0].strip()
163
+ raw_info = result[0].strip() if result else ""
131
164
  if raw_info.startswith("{") and raw_info.endswith("}"):
132
165
  info = eval(raw_info)
133
166
  self.family = info["family"]
@@ -140,17 +173,19 @@ class MPRemoteBoard:
140
173
  self.description = descr = info["board"]
141
174
  pos = descr.rfind(" with")
142
175
  short_descr = descr[:pos].strip() if pos != -1 else ""
143
- if info["_build"]:
144
- self.board = info["_build"].split('-')[0]
145
- self.variant = info["_build"].split('-')[1] if '-' in info["_build"] else ""
146
- elif board_name := find_board_id_by_description(
147
- descr, short_descr, version=self.version
148
- ):
149
- self.board = board_name
176
+ if info.get("board_id", None):
177
+ # we have a board_id - so use that to get the board name
178
+ self.board_id = info["board_id"]
150
179
  else:
151
- self.board = "UNKNOWN_BOARD"
180
+ self.board_id = f"{info['board']}-{info.get('variant', '')}"
181
+ board_name = find_board_id_by_description(
182
+ descr, short_descr, version=self.version
183
+ )
184
+ self.board_id = board_name or "UNKNOWN_BOARD"
185
+ # TODO: Get the variant as well
152
186
  # get the board_info.toml
153
187
  self.get_board_info_toml()
188
+ # TODO: get board_id from the toml file if it exists
154
189
  # now we know the board is connected
155
190
  self.connected = True
156
191
 
@@ -174,7 +209,9 @@ class MPRemoteBoard:
174
209
  log_errors=False,
175
210
  )
176
211
  except Exception as e:
177
- raise ConnectionError(f"Failed to get board_info.toml for {self.serialport}:") from e
212
+ raise ConnectionError(
213
+ f"Failed to get board_info.toml for {self.serialport}:"
214
+ ) from e
178
215
  # this is optional - so only parse if we got the file
179
216
  self.toml = {}
180
217
  if rc in [OK]: # sometimes we get an -9 ???
@@ -273,3 +310,30 @@ class MPRemoteBoard:
273
310
  with contextlib.suppress(ConnectionError, MPFlashError):
274
311
  self.get_mcu_info()
275
312
  break
313
+
314
+ def to_dict(self) -> dict:
315
+ """
316
+ Serialize the MPRemoteBoard object to JSON, including all attributes and readable properties.
317
+
318
+ Returns:
319
+ - str: A JSON string representation of the object.
320
+ """
321
+
322
+ def get_properties(obj):
323
+ """Helper function to get all readable properties."""
324
+ return {
325
+ name: getattr(obj, name)
326
+ for name in dir(obj.__class__)
327
+ if isinstance(getattr(obj.__class__, name, None), property)
328
+ }
329
+
330
+ # Combine instance attributes, readable properties, and private attributes
331
+ data = {**self.__dict__, **get_properties(self)}
332
+
333
+ # remove the path and firmware attibutes from the json output as they are always empty
334
+ del data["_board_id"] # dup of board_id
335
+ del data["connected"]
336
+ del data["path"]
337
+ del data["firmware"]
338
+
339
+ return data
@@ -3,7 +3,7 @@ import os
3
3
  import sys
4
4
 
5
5
 
6
- def _build(s):
6
+ def get_build(s):
7
7
  # extract build from sys.version or os.uname().version if available
8
8
  # sys.version: 'MicroPython v1.23.0-preview.6.g3d0b6276f'
9
9
  # sys.implementation.version: 'v1.13-103-gb137d064e'
@@ -49,10 +49,14 @@ def _info(): # type:() -> dict[str, str]
49
49
  except AttributeError:
50
50
  pass
51
51
  try:
52
- machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
53
- info["board"] = machine.strip()
54
- info["_build"] = sys.implementation._build if "_build" in dir(sys.implementation) else ""
55
- info["cpu"] = machine.split("with")[-1].strip() if "with" in machine else ""
52
+ _machine = sys.implementation._machine if "_machine" in dir(sys.implementation) else os.uname().machine # type: ignore
53
+ info["board"] = _machine.strip()
54
+ si_build = sys.implementation._build if "_build" in dir(sys.implementation) else ""
55
+ if si_build:
56
+ info["board"] = si_build.split("-")[0]
57
+ info["variant"] = si_build.split("-")[1] if "-" in si_build else ""
58
+ info["board_id"] = si_build
59
+ info["cpu"] = _machine.split("with")[-1].strip() if "with" in _machine else ""
56
60
  info["mpy"] = (
57
61
  sys.implementation._mpy
58
62
  if "_mpy" in dir(sys.implementation)
@@ -65,12 +69,12 @@ def _info(): # type:() -> dict[str, str]
65
69
 
66
70
  try:
67
71
  if hasattr(sys, "version"):
68
- info["build"] = _build(sys.version)
72
+ info["build"] = get_build(sys.version)
69
73
  elif hasattr(os, "uname"):
70
- info["build"] = _build(os.uname()[3]) # type: ignore
74
+ info["build"] = get_build(os.uname()[3]) # type: ignore
71
75
  if not info["build"]:
72
76
  # extract build from uname().release if available
73
- info["build"] = _build(os.uname()[2]) # type: ignore
77
+ info["build"] = get_build(os.uname()[2]) # type: ignore
74
78
  except (AttributeError, IndexError):
75
79
  pass
76
80
  # avoid build hashes
@@ -146,4 +150,4 @@ def _info(): # type:() -> dict[str, str]
146
150
 
147
151
 
148
152
  print(_info())
149
- del _info, _build, _version_str
153
+ del _info, get_build, _version_str