mpflash 1.25.0rc4__py3-none-any.whl → 1.25.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/basicgit.py +19 -0
- mpflash/cli_add.py +131 -0
- mpflash/cli_download.py +3 -14
- mpflash/cli_flash.py +44 -26
- mpflash/cli_group.py +1 -0
- mpflash/cli_list.py +2 -0
- mpflash/cli_main.py +7 -0
- mpflash/common.py +14 -2
- mpflash/config.py +22 -1
- mpflash/connected.py +8 -6
- mpflash/custom/__init__.py +144 -0
- mpflash/custom/naming.py +91 -0
- mpflash/db/core.py +84 -6
- mpflash/db/gather_boards.py +6 -4
- mpflash/db/loader.py +4 -3
- mpflash/db/meta.py +4 -3
- mpflash/db/micropython_boards.zip +0 -0
- mpflash/db/models.py +2 -0
- mpflash/download/__init__.py +58 -1
- mpflash/download/fwinfo.py +1 -1
- mpflash/download/jid.py +1 -1
- mpflash/downloaded.py +8 -4
- mpflash/flash/__init__.py +10 -1
- mpflash/flash/uf2/windows.py +1 -1
- mpflash/flash/worklist.py +40 -19
- mpflash/list.py +2 -0
- mpflash/logger.py +27 -7
- mpflash/mpboard_id/board_id.py +13 -17
- mpflash/mpboard_id/known.py +8 -2
- mpflash/mpremoteboard/__init__.py +61 -3
- mpflash/mpremoteboard/runner.py +1 -0
- {mpflash-1.25.0rc4.dist-info → mpflash-1.25.2.dist-info}/METADATA +63 -10
- {mpflash-1.25.0rc4.dist-info → mpflash-1.25.2.dist-info}/RECORD +36 -34
- {mpflash-1.25.0rc4.dist-info → mpflash-1.25.2.dist-info}/WHEEL +1 -1
- mpflash/add_firmware.py +0 -125
- {mpflash-1.25.0rc4.dist-info → mpflash-1.25.2.dist-info}/LICENSE +0 -0
- {mpflash-1.25.0rc4.dist-info → mpflash-1.25.2.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
import shutil
|
2
|
+
import sqlite3
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Union
|
5
|
+
|
6
|
+
import jsonlines
|
7
|
+
import requests
|
8
|
+
from loguru import logger as log
|
9
|
+
# re-use logic from mpremote
|
10
|
+
from mpremote.mip import _rewrite_url as rewrite_url # type: ignore
|
11
|
+
|
12
|
+
from mpflash.config import config
|
13
|
+
from mpflash.db.core import Session
|
14
|
+
from mpflash.db.models import Firmware
|
15
|
+
from mpflash.errors import MPFlashError
|
16
|
+
from mpflash.versions import get_preview_mp_version, get_stable_mp_version
|
17
|
+
|
18
|
+
from .naming import (custom_fw_from_path, extract_commit_count,
|
19
|
+
port_and_boardid_from_path)
|
20
|
+
|
21
|
+
#
|
22
|
+
# github.com/<owner>/<repo>@<branch>#<commit>
|
23
|
+
# $remote_url = git remote get-url origin
|
24
|
+
# $branch = git rev-parse --abbrev-ref HEAD
|
25
|
+
# $commit = git rev-parse --short HEAD
|
26
|
+
# if ($remote_url -match "github.com[:/](.+)/(.+?)(\.git)?$") {
|
27
|
+
# $owner = $matches[1]
|
28
|
+
# $repo = $matches[2]
|
29
|
+
# "github.com/$owner/$repo@$branch#$commit"
|
30
|
+
# }
|
31
|
+
|
32
|
+
|
33
|
+
# 1) local path
|
34
|
+
|
35
|
+
|
36
|
+
def add_firmware(
|
37
|
+
source: Path,
|
38
|
+
fw_info: dict,
|
39
|
+
*,
|
40
|
+
force: bool = False,
|
41
|
+
custom: bool = False,
|
42
|
+
) -> bool:
|
43
|
+
"""
|
44
|
+
Add a firmware to the database , and firmware folder.
|
45
|
+
stored in the port folder, with the filename.
|
46
|
+
|
47
|
+
fw_info is a dict with the following keys:
|
48
|
+
- board_id: str, required
|
49
|
+
- version: str, required
|
50
|
+
- port: str, required
|
51
|
+
- firmware_file: str, required, the filename to store in the firmware folder
|
52
|
+
- source: str, optional, the source of the firmware, can be a local path
|
53
|
+
- description: str, optional, a description of the firmware
|
54
|
+
- custom: bool, optional, if the firmware is a custom firmware, default False
|
55
|
+
"""
|
56
|
+
try:
|
57
|
+
source = source.expanduser().absolute()
|
58
|
+
if not source.exists() or not source.is_file():
|
59
|
+
log.error(f"Source file {source} does not exist or is not a file")
|
60
|
+
return False
|
61
|
+
with Session() as session:
|
62
|
+
# Check minimal info needed
|
63
|
+
new_fw = Firmware(**fw_info)
|
64
|
+
if custom:
|
65
|
+
new_fw.custom = True
|
66
|
+
|
67
|
+
if not new_fw.board_id:
|
68
|
+
log.error("board_id is required")
|
69
|
+
return False
|
70
|
+
|
71
|
+
# assume the the firmware_file has already been prepared
|
72
|
+
fw_filename = config.firmware_folder / new_fw.firmware_file
|
73
|
+
|
74
|
+
if not copy_firmware(source, fw_filename, force):
|
75
|
+
log.error(f"Failed to copy {source} to {fw_filename}")
|
76
|
+
return False
|
77
|
+
# add to inventory
|
78
|
+
# check if the firmware already exists
|
79
|
+
if custom:
|
80
|
+
qry = session.query(Firmware).filter(Firmware.custom_id == new_fw.custom_id)
|
81
|
+
else:
|
82
|
+
qry = session.query(Firmware).filter(Firmware.board_id == new_fw.board_id)
|
83
|
+
|
84
|
+
qry = qry.filter(
|
85
|
+
Firmware.board_id == new_fw.board_id,
|
86
|
+
Firmware.version == new_fw.version,
|
87
|
+
Firmware.port == new_fw.port,
|
88
|
+
Firmware.custom == new_fw.custom,
|
89
|
+
)
|
90
|
+
existing_fw = qry.first()
|
91
|
+
|
92
|
+
if existing_fw:
|
93
|
+
if not force:
|
94
|
+
log.warning(f"Firmware {existing_fw} already exists")
|
95
|
+
return False
|
96
|
+
# update the existing firmware
|
97
|
+
existing_fw.firmware_file = new_fw.firmware_file
|
98
|
+
existing_fw.source = new_fw.source
|
99
|
+
existing_fw.description = new_fw.description
|
100
|
+
existing_fw.custom = custom
|
101
|
+
if custom:
|
102
|
+
existing_fw.custom_id = new_fw.custom_id
|
103
|
+
else:
|
104
|
+
session.add(new_fw)
|
105
|
+
session.commit()
|
106
|
+
|
107
|
+
return True
|
108
|
+
except sqlite3.DatabaseError as e:
|
109
|
+
raise MPFlashError(
|
110
|
+
f"Failed to add firmware {fw_info['firmware_file']}: {e}"
|
111
|
+
) from e
|
112
|
+
|
113
|
+
|
114
|
+
def copy_firmware(source: Path, fw_filename: Path, force: bool = False):
|
115
|
+
"""Add a firmware to the firmware folder.
|
116
|
+
stored in the port folder, with the same filename as the source.
|
117
|
+
"""
|
118
|
+
if fw_filename.exists() and not force:
|
119
|
+
log.error(f" {fw_filename} already exists. Use --force to overwrite")
|
120
|
+
return False
|
121
|
+
fw_filename.parent.mkdir(parents=True, exist_ok=True)
|
122
|
+
if isinstance(source, Path):
|
123
|
+
if not source.exists():
|
124
|
+
log.error(f"File {source} does not exist")
|
125
|
+
return False
|
126
|
+
# file copy
|
127
|
+
log.debug(f"Copy {source} to {fw_filename}")
|
128
|
+
shutil.copy(source, fw_filename)
|
129
|
+
return True
|
130
|
+
# TODO: handle github urls
|
131
|
+
# url = rewrite_url(source)
|
132
|
+
# if str(source).startswith("http://") or str(source).startswith("https://"):
|
133
|
+
# log.debug(f"Download {url} to {fw_filename}")
|
134
|
+
# response = requests.get(url)
|
135
|
+
|
136
|
+
# if response.status_code == 200:
|
137
|
+
# with open(fw_filename, "wb") as file:
|
138
|
+
# file.write(response.content)
|
139
|
+
# log.info("File downloaded and saved successfully.")
|
140
|
+
# return True
|
141
|
+
# else:
|
142
|
+
# print("Failed to download the file.")
|
143
|
+
# return False
|
144
|
+
# return False
|
mpflash/custom/naming.py
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
import re
|
2
|
+
from pathlib import Path
|
3
|
+
from typing import Dict, List, Optional, Tuple, Union
|
4
|
+
|
5
|
+
import mpflash.basicgit as git
|
6
|
+
from mpflash.logger import log
|
7
|
+
|
8
|
+
|
9
|
+
def custom_fw_from_path(fw_path: Path) -> Dict[str, Union[str, int, bool]]:
|
10
|
+
"""Generate a custom name for the firmware file based on its path.
|
11
|
+
|
12
|
+
Args:
|
13
|
+
firmware_path: Path to firmware file
|
14
|
+
|
15
|
+
Returns:
|
16
|
+
Custom name for the firmware file
|
17
|
+
"""
|
18
|
+
repo_path = fw_path.expanduser().absolute().parent
|
19
|
+
port, board_id = port_and_boardid_from_path(fw_path)
|
20
|
+
if not port or not board_id:
|
21
|
+
raise ValueError(f"Could not extract port and board_id from path: {fw_path}")
|
22
|
+
if "wsl.localhost" in str(repo_path):
|
23
|
+
log.info("Accessing WSL path; please note that it may take a few seconds to get git info across filesystems")
|
24
|
+
version = git.get_local_tag(repo_path) or "unknown"
|
25
|
+
describe = git.get_git_describe(repo_path)
|
26
|
+
if describe:
|
27
|
+
build = extract_commit_count(describe)
|
28
|
+
else:
|
29
|
+
build = 0
|
30
|
+
branch = git.get_current_branch(repo_path)
|
31
|
+
if branch:
|
32
|
+
branch = branch.split("/")[-1] # Use only last part of the branch name (?)
|
33
|
+
build_str = f".{build}" if build > 0 else ""
|
34
|
+
branch_str = f"@{branch}" if branch else ""
|
35
|
+
new_fw_path = Path(port) / f"{board_id}{branch_str}-{version}{build_str}{fw_path.suffix}"
|
36
|
+
|
37
|
+
return {
|
38
|
+
"port": port,
|
39
|
+
"board_id": f"{board_id}",
|
40
|
+
"custom_id": f"{board_id}{branch_str}",
|
41
|
+
"version": version,
|
42
|
+
"build": build,
|
43
|
+
"custom": True,
|
44
|
+
"firmware_file": new_fw_path.as_posix(),
|
45
|
+
"source": fw_path.expanduser().absolute().as_uri() if isinstance(fw_path, Path) else fw_path, # Use URI for local files
|
46
|
+
}
|
47
|
+
|
48
|
+
|
49
|
+
def port_and_boardid_from_path(firmware_path: Path) -> Tuple[Optional[str], Optional[str]]:
|
50
|
+
"""Extract port and board_id from firmware path.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
firmware_path: Path to firmware file
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
Tuple of (port, board_id) or (None, None) if not found
|
57
|
+
"""
|
58
|
+
path_str = str(firmware_path).replace("\\", "/") # Normalize path for regex matching
|
59
|
+
|
60
|
+
# Pattern: /path/to/micropython/ports/{port}/build-{board_id}/firmware.ext
|
61
|
+
build_match = re.search(r"/ports/([^/]+)/build-([^/]+)/", path_str)
|
62
|
+
if build_match:
|
63
|
+
port = build_match.group(1)
|
64
|
+
board_id = build_match.group(2)
|
65
|
+
# Remove variant suffix (e.g., GENERIC_S3-SPIRAM_OCT -> GENERIC_S3)
|
66
|
+
board_id = board_id.split("-")[0]
|
67
|
+
return port, board_id
|
68
|
+
|
69
|
+
# Pattern: /path/to/micropython/ports/{port}/firmware.ext
|
70
|
+
port_match = re.search(r"/ports/([^/]+)/[^/]*firmware\.[^/]*$", path_str)
|
71
|
+
if port_match:
|
72
|
+
port = port_match.group(1)
|
73
|
+
return port, None
|
74
|
+
|
75
|
+
return None, None
|
76
|
+
|
77
|
+
|
78
|
+
def extract_commit_count(git_describe: str) -> int:
|
79
|
+
"""Extract commit count from git describe string.
|
80
|
+
|
81
|
+
Args:
|
82
|
+
git_describe: Git describe output like 'v1.26.0-preview-214-ga56a1eec7b-dirty'
|
83
|
+
|
84
|
+
Returns:
|
85
|
+
Commit count as integer or None if not found
|
86
|
+
"""
|
87
|
+
# Match patterns like v1.26.0-preview-214-g... or v1.26.0-214-g...
|
88
|
+
match = re.search(r"-(\d+)-g[a-f0-9]+", git_describe)
|
89
|
+
if match:
|
90
|
+
return int(match.group(1))
|
91
|
+
return 0
|
mpflash/db/core.py
CHANGED
@@ -2,7 +2,8 @@ from pathlib import Path
|
|
2
2
|
from sqlite3 import DatabaseError, OperationalError
|
3
3
|
|
4
4
|
from loguru import logger as log
|
5
|
-
from sqlalchemy import create_engine
|
5
|
+
from sqlalchemy import create_engine, text
|
6
|
+
from sqlalchemy.exc import SQLAlchemyError
|
6
7
|
from sqlalchemy.orm import sessionmaker
|
7
8
|
|
8
9
|
from mpflash.config import config
|
@@ -13,18 +14,90 @@ from .models import Base
|
|
13
14
|
|
14
15
|
TRACE = False
|
15
16
|
connect_str = f"sqlite:///{config.db_path.as_posix()}"
|
17
|
+
log.debug(f"Connecting to database at {connect_str}")
|
16
18
|
engine = create_engine(connect_str, echo=TRACE)
|
17
19
|
Session = sessionmaker(bind=engine)
|
18
20
|
|
19
21
|
|
22
|
+
def get_schema_version() -> int:
|
23
|
+
"""Get current database schema version."""
|
24
|
+
from .meta import get_metadata_value
|
25
|
+
|
26
|
+
version = get_metadata_value("schema_version")
|
27
|
+
return int(version) if version else 0
|
28
|
+
|
29
|
+
|
30
|
+
def set_schema_version(version: int) -> None:
|
31
|
+
"""Set database schema version."""
|
32
|
+
from .meta import set_metadata_value
|
33
|
+
|
34
|
+
set_metadata_value("schema_version", str(version))
|
35
|
+
|
36
|
+
|
37
|
+
def migration_001_add_custom_id() -> None:
|
38
|
+
"""Add custom_id column to firmwares table."""
|
39
|
+
log.info("Running migration 001: Add custom_id column to firmwares table")
|
40
|
+
|
41
|
+
with Session() as session:
|
42
|
+
# Check if column already exists
|
43
|
+
result = session.execute(text("PRAGMA table_info(firmwares)")).fetchall()
|
44
|
+
|
45
|
+
columns = [row[1] for row in result]
|
46
|
+
if "custom_id" not in columns:
|
47
|
+
# Add column without UNIQUE constraint
|
48
|
+
session.execute(text("ALTER TABLE firmwares ADD COLUMN custom_id VARCHAR(40)"))
|
49
|
+
|
50
|
+
# Create regular index for performance
|
51
|
+
session.execute(text("CREATE INDEX IF NOT EXISTS idx_firmwares_custom_id ON firmwares(custom_id)"))
|
52
|
+
|
53
|
+
session.commit()
|
54
|
+
log.info("Added custom_id column and index to firmwares table")
|
55
|
+
else:
|
56
|
+
log.info("custom_id column already exists in firmwares table")
|
57
|
+
|
58
|
+
# Check if index exists and create if needed
|
59
|
+
result = session.execute(text("PRAGMA index_list(firmwares)")).fetchall()
|
60
|
+
|
61
|
+
index_names = [row[1] for row in result]
|
62
|
+
if "idx_firmwares_custom_id" not in index_names:
|
63
|
+
session.execute(text("CREATE INDEX IF NOT EXISTS idx_firmwares_custom_id ON firmwares(custom_id)"))
|
64
|
+
session.commit()
|
65
|
+
log.info("Added index to existing custom_id column")
|
66
|
+
|
67
|
+
|
68
|
+
def run_schema_migrations() -> None:
|
69
|
+
"""Run all pending database schema migrations."""
|
70
|
+
current_version = get_schema_version()
|
71
|
+
target_version = 1 # Current latest version
|
72
|
+
|
73
|
+
if current_version >= target_version:
|
74
|
+
log.debug(f"Database schema is up to date (version {current_version})")
|
75
|
+
return
|
76
|
+
|
77
|
+
log.info(f"Upgrading database schema from version {current_version} to {target_version}")
|
78
|
+
|
79
|
+
try:
|
80
|
+
# Run migrations in order
|
81
|
+
if current_version < 1:
|
82
|
+
migration_001_add_custom_id()
|
83
|
+
|
84
|
+
# Update schema version
|
85
|
+
set_schema_version(target_version)
|
86
|
+
log.info(f"Database schema upgraded to version {target_version}")
|
87
|
+
|
88
|
+
except SQLAlchemyError as e:
|
89
|
+
log.error(f"Migration failed: {e}")
|
90
|
+
raise MPFlashError(f"Failed to migrate database schema: {e}")
|
91
|
+
|
92
|
+
|
20
93
|
def migrate_database(boards: bool = True, firmwares: bool = True):
|
21
|
-
"""Migrate from 1.24.x to 1.25.x"""
|
94
|
+
"""Migrate from 1.24.x to 1.25.x and run schema migrations."""
|
22
95
|
# Move import here to avoid circular import
|
23
96
|
from .loader import load_jsonl_to_db, update_boards
|
24
97
|
|
25
98
|
# get the location of the database from the session
|
26
99
|
with Session() as session:
|
27
|
-
db_location = session.get_bind().url.database
|
100
|
+
db_location = session.get_bind().url.database # type: ignore
|
28
101
|
log.debug(f"Database location: {Path(db_location)}") # type: ignore
|
29
102
|
|
30
103
|
try:
|
@@ -33,6 +106,10 @@ def migrate_database(boards: bool = True, firmwares: bool = True):
|
|
33
106
|
log.error(f"Error creating database: {e}")
|
34
107
|
log.error("Database might already exist, trying to migrate.")
|
35
108
|
raise MPFlashError("Database migration failed. Please check the logs for more details.") from e
|
109
|
+
|
110
|
+
# Run schema migrations after creating database
|
111
|
+
run_schema_migrations()
|
112
|
+
|
36
113
|
if boards:
|
37
114
|
update_boards()
|
38
115
|
if firmwares:
|
@@ -54,8 +131,9 @@ def migrate_database(boards: bool = True, firmwares: bool = True):
|
|
54
131
|
|
55
132
|
|
56
133
|
def create_database():
|
57
|
-
"""
|
58
|
-
Create the SQLite database and tables if they don't exist.
|
59
|
-
"""
|
134
|
+
"""Create the SQLite database and tables if they don't exist."""
|
60
135
|
# Create the database and tables if they don't exist
|
61
136
|
Base.metadata.create_all(engine)
|
137
|
+
|
138
|
+
# Run schema migrations after table creation
|
139
|
+
run_schema_migrations()
|
mpflash/db/gather_boards.py
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
from os import path
|
2
2
|
from pathlib import Path
|
3
|
-
from mpflash.mpremoteboard import HERE
|
4
|
-
from mpflash.vendor.board_database import Database
|
5
|
-
from mpflash.logger import log
|
6
|
-
|
7
3
|
from typing import List
|
4
|
+
|
8
5
|
import mpflash.basicgit as git
|
6
|
+
from mpflash.logger import log
|
7
|
+
from mpflash.mpremoteboard import HERE
|
8
|
+
from mpflash.vendor.board_database import Database
|
9
9
|
from mpflash.versions import micropython_versions
|
10
10
|
|
11
11
|
HERE = Path(__file__).parent.resolve()
|
@@ -79,6 +79,7 @@ def create_zip_file(longlist, zip_file: Path):
|
|
79
79
|
"""Create a ZIP file containing the CSV data."""
|
80
80
|
# lazy import
|
81
81
|
import zipfile
|
82
|
+
|
82
83
|
import pandas as pd
|
83
84
|
|
84
85
|
csv_filename = "micropython_boards.csv"
|
@@ -96,6 +97,7 @@ def create_zip_file(longlist, zip_file: Path):
|
|
96
97
|
|
97
98
|
def package_repo(mpy_path: Path):
|
98
99
|
mpy_path = mpy_path or Path("../repos/micropython")
|
100
|
+
log.info(f"Packaging Micropython boards from {mpy_path}")
|
99
101
|
mp_versions = micropython_versions(minver="1.18")
|
100
102
|
longlist = boardlist_from_repo(
|
101
103
|
versions=mp_versions,
|
mpflash/db/loader.py
CHANGED
@@ -24,7 +24,7 @@ def load_data_from_zip(zip_file: Path) -> int:
|
|
24
24
|
if not zip_file.exists() or not zip_file.is_file():
|
25
25
|
log.error(f"Zip file {zip_file} not found.")
|
26
26
|
return 0
|
27
|
-
count = 0
|
27
|
+
count = 0
|
28
28
|
# Load data directly from the zip file
|
29
29
|
with zipfile.ZipFile(zip_file, "r") as zipf:
|
30
30
|
# Read the CSV file from the zip
|
@@ -109,14 +109,15 @@ def load_jsonl_to_db(jsonl_path: Path):
|
|
109
109
|
|
110
110
|
|
111
111
|
def update_boards():
|
112
|
+
boards_version = "v1.25.2"
|
112
113
|
try:
|
113
114
|
meta = get_metadata()
|
114
115
|
log.debug(f"Metadata: {meta}")
|
115
|
-
if meta.get("boards_version", "") <
|
116
|
+
if meta.get("boards_version", "") < boards_version:
|
116
117
|
log.info("Update boards from CSV to SQLite database.")
|
117
118
|
# Load data from the zip file into the database
|
118
119
|
load_data_from_zip(HERE / "micropython_boards.zip")
|
119
|
-
set_metadata_value("boards_version",
|
120
|
+
set_metadata_value("boards_version", boards_version)
|
120
121
|
meta = get_metadata()
|
121
122
|
except Exception as e:
|
122
123
|
raise MPFlashError(f"Error updating boards table: {e}") from e
|
mpflash/db/meta.py
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
from typing import Optional
|
3
2
|
|
4
3
|
from loguru import logger as log
|
@@ -18,10 +17,11 @@ def get_metadata() -> dict:
|
|
18
17
|
with Session() as session:
|
19
18
|
metadata = session.query(Metadata).all()
|
20
19
|
return {m.name: m.value for m in metadata}
|
21
|
-
except (DatabaseError, OperationalError)
|
20
|
+
except (DatabaseError, OperationalError) as e:
|
22
21
|
log.error(f"Error retrieving metadata: {e}")
|
23
22
|
return {}
|
24
|
-
|
23
|
+
|
24
|
+
|
25
25
|
def set_metadata(metadata: dict):
|
26
26
|
"""
|
27
27
|
Set metadata in the database.
|
@@ -56,6 +56,7 @@ def get_metadata_value(name: str) -> Optional[str]:
|
|
56
56
|
metadata = session.query(Metadata).filter(Metadata.name == name).first()
|
57
57
|
return metadata.value if metadata else None
|
58
58
|
|
59
|
+
|
59
60
|
def set_metadata_value(name: str, value: str):
|
60
61
|
"""
|
61
62
|
Set metadata value by name.
|
Binary file
|
mpflash/db/models.py
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
from pathlib import Path
|
2
|
+
from typing import Union
|
2
3
|
|
3
4
|
import sqlalchemy as sa
|
4
5
|
from sqlalchemy import Index, String
|
@@ -78,6 +79,7 @@ class Firmware(Base):
|
|
78
79
|
source: Mapped[str] = mapped_column()
|
79
80
|
build: Mapped[int] = mapped_column(default=0, comment="Build number")
|
80
81
|
custom: Mapped[bool] = mapped_column(default=False, comment="True if this is a custom firmware")
|
82
|
+
custom_id: Mapped[Union[str, None]] = mapped_column(String(40), nullable=True, default=None)
|
81
83
|
|
82
84
|
@property
|
83
85
|
def preview(self) -> bool:
|
mpflash/download/__init__.py
CHANGED
@@ -4,6 +4,7 @@ Uses the micropython.org website to get the available versions and locations to
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
import itertools
|
7
|
+
import re
|
7
8
|
from pathlib import Path
|
8
9
|
from typing import Dict, List, Optional
|
9
10
|
|
@@ -16,7 +17,7 @@ from rich.progress import track
|
|
16
17
|
from mpflash.common import PORT_FWTYPES
|
17
18
|
from mpflash.config import config
|
18
19
|
from mpflash.db.core import Session
|
19
|
-
from mpflash.db.models import Firmware
|
20
|
+
from mpflash.db.models import Firmware, Board
|
20
21
|
from mpflash.downloaded import clean_downloaded_firmwares
|
21
22
|
from mpflash.errors import MPFlashError
|
22
23
|
from mpflash.mpboard_id.alternate import add_renamed_boards
|
@@ -36,6 +37,56 @@ def key_fw_boardid_preview_ext(fw: Firmware):
|
|
36
37
|
return fw.board_id, fw.preview, fw.ext
|
37
38
|
|
38
39
|
|
40
|
+
|
41
|
+
|
42
|
+
# Cache for variant suffixes - will be populated on first use
|
43
|
+
_PATTERN = ""
|
44
|
+
|
45
|
+
def _get_variant_pattern():
|
46
|
+
"""
|
47
|
+
Query the database for all known variant suffixes.
|
48
|
+
This is done only once and the result is cached.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
List of known variant suffixes.
|
52
|
+
"""
|
53
|
+
global _PATTERN
|
54
|
+
|
55
|
+
if _PATTERN:
|
56
|
+
# If the pattern is already set, return it
|
57
|
+
return _PATTERN
|
58
|
+
|
59
|
+
with Session() as session:
|
60
|
+
# Query distinct variants
|
61
|
+
variants = session.query(Board.variant).distinct().all()
|
62
|
+
|
63
|
+
|
64
|
+
_VARIANT_SUFFIXES = [f"_{v[0]}" for v in variants if v[0] and v[0].strip()]
|
65
|
+
# workaround for the fact that the variant names do not always match the suffixes
|
66
|
+
# e.g. 'PIMORONI_PICOLIPO' has the variant 'FLASH_16MB' but the suffix is '_16MB'
|
67
|
+
_VARIANT_SUFFIXES.extend( ["_2MB", "_4MB", "_8MB", "_16MB"] ) # add common SPIRAM size suffixes
|
68
|
+
|
69
|
+
# return _VARIANT_SUFFIXES
|
70
|
+
_PATTERN = f"({'|'.join(re.escape(v) for v in _VARIANT_SUFFIXES)})$"
|
71
|
+
return _PATTERN
|
72
|
+
|
73
|
+
|
74
|
+
|
75
|
+
def strip_variant(board: str) -> str:
|
76
|
+
"""
|
77
|
+
Strips the variant suffix from the board name based on variants in the database.
|
78
|
+
For example, 'RPI_PICO_W_SPIRAM' becomes 'RPI_PICO_W'.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
board: The board name to process.
|
82
|
+
|
83
|
+
Returns:
|
84
|
+
The board name without the variant suffix.
|
85
|
+
"""
|
86
|
+
pattern = _get_variant_pattern()
|
87
|
+
return re.sub(pattern, "", board)
|
88
|
+
|
89
|
+
|
39
90
|
def download_firmwares(
|
40
91
|
firmware_folder: Path,
|
41
92
|
ports: List[str],
|
@@ -60,9 +111,15 @@ def download_firmwares(
|
|
60
111
|
|
61
112
|
downloaded = 0
|
62
113
|
versions = [] if versions is None else [clean_version(v) for v in versions]
|
114
|
+
|
115
|
+
# remove the known variant suffixes from the boards
|
116
|
+
# TODO: IS THIS REALLY NEEDED ?
|
117
|
+
# boards = [strip_variant(b) for b in boards]
|
118
|
+
|
63
119
|
# handle downloading firmware for renamed boards
|
64
120
|
boards = add_renamed_boards(boards)
|
65
121
|
|
122
|
+
|
66
123
|
available_firmwares = get_firmware_list(ports, boards, versions, clean)
|
67
124
|
|
68
125
|
for b in available_firmwares:
|
mpflash/download/fwinfo.py
CHANGED
mpflash/download/jid.py
CHANGED
@@ -48,7 +48,7 @@ def ensure_firmware_downloaded(worklist: WorkList, version: str, force: bool) ->
|
|
48
48
|
newlist.append((mcu, new_firmware[0]))
|
49
49
|
else:
|
50
50
|
log.info(f"Found {version} firmware {board_firmwares[-1].firmware_file} for {mcu.board} on {mcu.serialport}.")
|
51
|
-
newlist.append((mcu,
|
51
|
+
newlist.append((mcu, board_firmwares[0]))
|
52
52
|
|
53
53
|
worklist.clear()
|
54
54
|
worklist.extend(newlist)
|
mpflash/downloaded.py
CHANGED
@@ -59,17 +59,22 @@ def find_downloaded_firmware(
|
|
59
59
|
version: str = "",
|
60
60
|
port: str = "",
|
61
61
|
variants: bool = False,
|
62
|
+
custom: bool = False,
|
62
63
|
) -> List[Firmware]:
|
63
64
|
version = clean_version(version)
|
64
65
|
log.debug(f"Looking for firmware for {board_id} {version} ")
|
65
66
|
# Special handling for preview versions
|
66
67
|
with Session() as session:
|
67
|
-
if
|
68
|
+
if "preview" in version:
|
68
69
|
# Find all preview firmwares for this board/port, return the latest (highest build)
|
69
|
-
|
70
|
+
if custom:
|
71
|
+
query = session.query(Firmware).filter(Firmware.custom_id == board_id)
|
72
|
+
else:
|
73
|
+
query = session.query(Firmware).filter(Firmware.board_id == board_id)
|
70
74
|
if port:
|
71
75
|
query = query.filter(Firmware.port == port)
|
72
76
|
query = query.filter(Firmware.firmware_file.contains("preview")).order_by(Firmware.build.desc())
|
77
|
+
log.trace(f"Querying for preview firmware: {query}")
|
73
78
|
fw_list = query.all()
|
74
79
|
if fw_list:
|
75
80
|
return [fw_list[0]] # Return the latest preview only
|
@@ -82,7 +87,7 @@ def find_downloaded_firmware(
|
|
82
87
|
#
|
83
88
|
log.debug(f"2nd search with renamed board_id :{board_id}")
|
84
89
|
with Session() as session:
|
85
|
-
if
|
90
|
+
if "preview" in version:
|
86
91
|
query = session.query(Firmware).filter(Firmware.board_id.in_(more_board_ids))
|
87
92
|
if port:
|
88
93
|
query = query.filter(Firmware.port == port)
|
@@ -96,4 +101,3 @@ def find_downloaded_firmware(
|
|
96
101
|
return fw_list
|
97
102
|
log.warning(f"No firmware files found for board {board_id} version {version}")
|
98
103
|
return []
|
99
|
-
|
mpflash/flash/__init__.py
CHANGED
@@ -39,6 +39,15 @@ def flash_list(
|
|
39
39
|
log.error(f"Failed to flash {mcu.board} on {mcu.serialport}: {e}")
|
40
40
|
continue
|
41
41
|
if updated:
|
42
|
+
if fw_info.custom:
|
43
|
+
# Add / Update board_info.toml with the custom_id and Description
|
44
|
+
mcu.get_board_info_toml()
|
45
|
+
if fw_info.description:
|
46
|
+
mcu.toml["description"] = fw_info.description
|
47
|
+
mcu.toml["mpflash"]["board_id"] = fw_info.board_id
|
48
|
+
mcu.toml["mpflash"]["custom_id"] = fw_info.custom_id
|
49
|
+
mcu.set_board_info_toml()
|
50
|
+
|
42
51
|
flashed.append(updated)
|
43
52
|
else:
|
44
53
|
log.error(f"Failed to flash {mcu.board} on {mcu.serialport}")
|
@@ -51,7 +60,7 @@ def flash_mcu(
|
|
51
60
|
fw_file: Path,
|
52
61
|
erase: bool = False,
|
53
62
|
bootloader: BootloaderMethod = BootloaderMethod.AUTO,
|
54
|
-
**kwargs
|
63
|
+
**kwargs
|
55
64
|
):
|
56
65
|
"""Flash a single MCU with the specified firmware."""
|
57
66
|
updated = None
|
mpflash/flash/uf2/windows.py
CHANGED
@@ -7,6 +7,7 @@ import time
|
|
7
7
|
from pathlib import Path
|
8
8
|
from typing import Optional
|
9
9
|
|
10
|
+
import psutil
|
10
11
|
from rich.progress import track
|
11
12
|
|
12
13
|
from .boardid import get_board_id
|
@@ -14,7 +15,6 @@ from .boardid import get_board_id
|
|
14
15
|
|
15
16
|
def wait_for_UF2_windows(board_id: str, s_max: int = 10) -> Optional[Path]:
|
16
17
|
"""Wait for the MCU to mount as a drive"""
|
17
|
-
import psutil
|
18
18
|
|
19
19
|
if s_max < 1:
|
20
20
|
s_max = 10
|