sima-cli 0.0.21__py3-none-any.whl → 0.0.23__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.
- sima_cli/__version__.py +1 -1
- sima_cli/cli.py +73 -10
- sima_cli/download/downloader.py +7 -3
- sima_cli/install/metadata_info.py +57 -0
- sima_cli/install/metadata_installer.py +447 -0
- sima_cli/install/metadata_validator.py +138 -0
- sima_cli/network/network.py +15 -0
- sima_cli/{nvme → storage}/nvme.py +52 -21
- sima_cli/storage/sdcard.py +136 -0
- sima_cli/update/bootimg.py +11 -6
- sima_cli/update/query.py +49 -26
- sima_cli/update/remote.py +8 -6
- sima_cli/update/updater.py +43 -14
- sima_cli/utils/disk.py +15 -0
- sima_cli/utils/env.py +24 -0
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/METADATA +2 -1
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/RECORD +21 -16
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.21.dist-info → sima_cli-0.0.23.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,7 @@
|
|
1
1
|
import subprocess
|
2
2
|
import click
|
3
|
+
import re
|
4
|
+
|
3
5
|
from sima_cli.utils.env import is_modalix_devkit
|
4
6
|
|
5
7
|
def scan_nvme():
|
@@ -33,31 +35,68 @@ def format_nvme(lbaf_index):
|
|
33
35
|
|
34
36
|
def add_nvme_to_fstab():
|
35
37
|
"""
|
36
|
-
Add /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /mnt/nvme.
|
38
|
+
Add UUID-based entry for /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /mnt/nvme.
|
37
39
|
Only appends if the entry does not already exist.
|
38
|
-
Requires root permission to modify /etc/fstab.
|
40
|
+
Requires root permission to run blkid and modify /etc/fstab.
|
39
41
|
"""
|
42
|
+
device = "/dev/nvme0n1p1"
|
40
43
|
fstab_path = "/etc/fstab"
|
41
|
-
nvme_entry = "/dev/nvme0n1p1 /mnt/nvme ext4 defaults 0 2"
|
42
44
|
|
43
45
|
try:
|
44
|
-
#
|
46
|
+
# Get UUID and filesystem type using blkid
|
47
|
+
blkid_output = subprocess.check_output(["sudo", "blkid", device], text=True)
|
48
|
+
uuid_match = re.search(r'UUID="([^"]+)"', blkid_output)
|
49
|
+
type_match = re.search(r'TYPE="([^"]+)"', blkid_output)
|
50
|
+
|
51
|
+
if not uuid_match or not type_match:
|
52
|
+
click.echo("❌ Could not extract UUID or TYPE from blkid output.")
|
53
|
+
return
|
54
|
+
|
55
|
+
uuid = uuid_match.group(1)
|
56
|
+
fs_type = type_match.group(1)
|
57
|
+
|
58
|
+
# Construct the fstab line
|
59
|
+
fstab_entry = f"UUID={uuid} /mnt/nvme {fs_type} defaults,noatime,noauto 0 0"
|
60
|
+
|
61
|
+
# Check if entry already exists
|
45
62
|
with open(fstab_path, "r") as f:
|
46
63
|
for line in f:
|
47
|
-
if
|
48
|
-
click.echo("ℹ️ NVMe mount
|
64
|
+
if uuid in line or "/mnt/nvme" in line:
|
65
|
+
click.echo("ℹ️ NVMe UUID or mount point already exists in /etc/fstab.")
|
49
66
|
return
|
50
67
|
|
51
|
-
# Append the entry
|
52
|
-
append_cmd = f"echo '{
|
68
|
+
# Append the entry to fstab
|
69
|
+
append_cmd = f"echo '{fstab_entry}' | sudo tee -a {fstab_path} > /dev/null"
|
53
70
|
subprocess.run(append_cmd, shell=True, check=True)
|
54
|
-
click.echo("✅
|
71
|
+
click.echo(f"✅ Added NVMe UUID entry to /etc/fstab:\n{fstab_entry}")
|
72
|
+
|
73
|
+
except subprocess.CalledProcessError as e:
|
74
|
+
click.echo(f"❌ Failed to run blkid or append to fstab: {e}")
|
55
75
|
except Exception as e:
|
56
|
-
click.echo(f"❌
|
76
|
+
click.echo(f"❌ Unexpected error: {e}")
|
57
77
|
|
58
78
|
def mount_nvme():
|
59
|
-
|
60
|
-
|
79
|
+
try:
|
80
|
+
# Create mount point
|
81
|
+
subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
|
82
|
+
|
83
|
+
# Mount the NVMe partition
|
84
|
+
subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
|
85
|
+
|
86
|
+
add_nvme_to_fstab()
|
87
|
+
|
88
|
+
subprocess.run("sudo mount -a", shell=True, check=True)
|
89
|
+
|
90
|
+
# Change ownership to user 'sima'
|
91
|
+
subprocess.run("sudo chown sima:sima /mnt/nvme", shell=True, check=True)
|
92
|
+
|
93
|
+
subprocess.run("sudo chmod 755 /mnt/nvme", shell=True, check=True)
|
94
|
+
|
95
|
+
|
96
|
+
print("✅ NVMe mounted and write permission granted to user 'sima'.")
|
97
|
+
|
98
|
+
except subprocess.CalledProcessError as e:
|
99
|
+
print(f"❌ Error during NVMe mount: {e}")
|
61
100
|
|
62
101
|
def nvme_format():
|
63
102
|
if not is_modalix_devkit():
|
@@ -87,8 +126,6 @@ def nvme_format():
|
|
87
126
|
# Format and mount
|
88
127
|
format_nvme(lbaf_index)
|
89
128
|
mount_nvme()
|
90
|
-
add_nvme_to_fstab()
|
91
|
-
|
92
129
|
click.echo("✅ NVMe drive formatted and mounted at /mnt/nvme.")
|
93
130
|
except subprocess.CalledProcessError:
|
94
131
|
click.echo("❌ Formatting process failed.")
|
@@ -100,13 +137,7 @@ def nvme_remount():
|
|
100
137
|
return
|
101
138
|
|
102
139
|
try:
|
103
|
-
|
104
|
-
subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
|
105
|
-
# Mount the partition
|
106
|
-
subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
|
107
|
-
|
108
|
-
# Add NVME to fstab
|
109
|
-
add_nvme_to_fstab()
|
140
|
+
mount_nvme()
|
110
141
|
|
111
142
|
except subprocess.CalledProcessError as e:
|
112
143
|
raise RuntimeError(f"Failed to remount NVMe: {e}")
|
@@ -0,0 +1,136 @@
|
|
1
|
+
import click
|
2
|
+
import subprocess
|
3
|
+
import platform
|
4
|
+
import shutil
|
5
|
+
import sys
|
6
|
+
import os
|
7
|
+
import time
|
8
|
+
|
9
|
+
from sima_cli.update.bootimg import list_removable_devices, unmount_device, _require_sudo
|
10
|
+
from sima_cli.utils.env import is_sima_board
|
11
|
+
|
12
|
+
def get_partition_path(device: str) -> str:
|
13
|
+
"""For Linux: partition is device + '1'"""
|
14
|
+
return device + "1"
|
15
|
+
|
16
|
+
|
17
|
+
def find_mkfs_ext4() -> str:
|
18
|
+
"""Find mkfs.ext4 on Linux"""
|
19
|
+
mkfs_path = shutil.which("mkfs.ext4")
|
20
|
+
if mkfs_path and os.path.exists(mkfs_path):
|
21
|
+
return mkfs_path
|
22
|
+
return None
|
23
|
+
|
24
|
+
|
25
|
+
def kill_partition_users(device_path: str):
|
26
|
+
"""Kill processes using the partitions on the device"""
|
27
|
+
try:
|
28
|
+
output = subprocess.check_output(["lsblk", "-n", "-o", "NAME", device_path]).decode().strip().splitlines()
|
29
|
+
partitions = [f"/dev/{line.strip()}" for line in output if line.strip() and f"/dev/{line.strip()}" != device_path]
|
30
|
+
|
31
|
+
for p in partitions:
|
32
|
+
try:
|
33
|
+
users = subprocess.check_output(["sudo", "lsof", p], stderr=subprocess.DEVNULL).decode().splitlines()
|
34
|
+
pids = {line.split()[1] for line in users[1:] if line.strip()}
|
35
|
+
for pid in pids:
|
36
|
+
subprocess.run(["sudo", "kill", "-9", pid], check=False)
|
37
|
+
except subprocess.CalledProcessError:
|
38
|
+
# lsof exits 1 if no process is using the file
|
39
|
+
continue
|
40
|
+
|
41
|
+
# Let the system settle
|
42
|
+
time.sleep(1)
|
43
|
+
|
44
|
+
except Exception as e:
|
45
|
+
click.echo(f"⚠️ Could not resolve partition users: {e}")
|
46
|
+
|
47
|
+
def create_partition_table(device_path: str):
|
48
|
+
"""Linux: create a GPT partition table with one ext4 partition"""
|
49
|
+
click.echo(f"🧹 Wiping and partitioning {device_path} using parted (Linux)")
|
50
|
+
subprocess.run(["sudo", "parted", "-s", device_path, "mklabel", "gpt"], check=True)
|
51
|
+
subprocess.run(["sudo", "parted", "-s", device_path, "mkpart", "primary", "ext4", "0%", "100%"], check=True)
|
52
|
+
subprocess.run(["sudo", "partprobe", device_path], check=True)
|
53
|
+
|
54
|
+
|
55
|
+
def force_release_device(device_path: str):
|
56
|
+
"""
|
57
|
+
Try to forcefully release a device by killing users and removing mappings.
|
58
|
+
"""
|
59
|
+
# Ensure partitions like /dev/sdc1 don't block parted
|
60
|
+
subprocess.run(["sudo", "umount", device_path + "1"], stderr=subprocess.DEVNULL)
|
61
|
+
subprocess.run(["sudo", "dmsetup", "remove", device_path], stderr=subprocess.DEVNULL)
|
62
|
+
|
63
|
+
# Try lsof to find open handles
|
64
|
+
try:
|
65
|
+
out = subprocess.check_output(["sudo", "lsof", device_path], stderr=subprocess.DEVNULL).decode()
|
66
|
+
for line in out.splitlines()[1:]:
|
67
|
+
pid = line.split()[1]
|
68
|
+
subprocess.run(["sudo", "kill", "-9", pid], check=False)
|
69
|
+
except subprocess.CalledProcessError:
|
70
|
+
# Likely no users
|
71
|
+
pass
|
72
|
+
|
73
|
+
# Final probe reset
|
74
|
+
subprocess.run(["sudo", "partprobe"], stderr=subprocess.DEVNULL)
|
75
|
+
subprocess.run(["sudo", "udevadm", "settle"], stderr=subprocess.DEVNULL)
|
76
|
+
|
77
|
+
|
78
|
+
def sdcard_format():
|
79
|
+
"""Linux-only SD card formatter for ext4."""
|
80
|
+
|
81
|
+
if platform.system() != "Linux":
|
82
|
+
click.echo("❌ This command only supports Desktop Linux.")
|
83
|
+
sys.exit(1)
|
84
|
+
|
85
|
+
if is_sima_board():
|
86
|
+
click.echo("❌ This command does not run on the DevKit due to lack of mkfs.ext4 support.")
|
87
|
+
|
88
|
+
mkfs_path = find_mkfs_ext4()
|
89
|
+
if not mkfs_path:
|
90
|
+
click.echo("❌ mkfs.ext4 not found on this platform.")
|
91
|
+
sys.exit(1)
|
92
|
+
|
93
|
+
devices = list_removable_devices()
|
94
|
+
if not devices:
|
95
|
+
click.echo("⚠️ No removable SD card found.")
|
96
|
+
return
|
97
|
+
|
98
|
+
click.echo("\n🔍 Detected removable devices:")
|
99
|
+
for i, d in enumerate(devices):
|
100
|
+
click.echo(f"[{i}] {d['path']} - {d['size']} - {d['name']}")
|
101
|
+
|
102
|
+
selected_path = None
|
103
|
+
if len(devices) == 1:
|
104
|
+
if click.confirm(f"\n✅ Use device {devices[0]['path']}?"):
|
105
|
+
selected_path = devices[0]['path']
|
106
|
+
else:
|
107
|
+
choice = click.prompt("Enter the number of the device to format", type=int)
|
108
|
+
if 0 <= choice < len(devices):
|
109
|
+
selected_path = devices[choice]['path']
|
110
|
+
|
111
|
+
if not selected_path:
|
112
|
+
click.echo("❌ No device selected. Operation cancelled.")
|
113
|
+
return
|
114
|
+
|
115
|
+
click.echo(f"\n🚨 WARNING: This will ERASE ALL DATA on {selected_path}")
|
116
|
+
if not click.confirm("Are you sure you want to continue?"):
|
117
|
+
click.echo("❌ Aborted by user.")
|
118
|
+
return
|
119
|
+
|
120
|
+
_require_sudo()
|
121
|
+
unmount_device(selected_path)
|
122
|
+
force_release_device(selected_path)
|
123
|
+
kill_partition_users(selected_path)
|
124
|
+
|
125
|
+
try:
|
126
|
+
create_partition_table(selected_path)
|
127
|
+
partition_path = get_partition_path(selected_path)
|
128
|
+
|
129
|
+
click.echo(f"🧱 Formatting partition {partition_path} as ext4 using {mkfs_path}")
|
130
|
+
subprocess.run(["sudo", mkfs_path, "-F", partition_path], check=True)
|
131
|
+
|
132
|
+
click.echo(f"✅ Successfully formatted {partition_path} as ext4, insert this SD card into MLSoC or Modalix Early Access Kit")
|
133
|
+
|
134
|
+
except subprocess.CalledProcessError as e:
|
135
|
+
click.echo(f"❌ Formatting failed: {e}")
|
136
|
+
sys.exit(1)
|
sima_cli/update/bootimg.py
CHANGED
@@ -306,7 +306,7 @@ def write_image(version: str, board: str, swtype: str, internal: bool = False, f
|
|
306
306
|
Parameters:
|
307
307
|
version (str): Firmware version to download (e.g., "1.6.0").
|
308
308
|
board (str): Target board type, e.g., "modalix" or "mlsoc".
|
309
|
-
swtype (str): Software image type, e.g., "yocto" or "
|
309
|
+
swtype (str): Software image type, e.g., "yocto" or "elxr".
|
310
310
|
internal (bool): Whether to use internal download sources. Defaults to False.
|
311
311
|
flavor (str): Flavor of the software package - can be either headless or full.
|
312
312
|
|
@@ -318,17 +318,22 @@ def write_image(version: str, board: str, swtype: str, internal: bool = False, f
|
|
318
318
|
file_list = download_image(version, board, swtype, internal, update_type='bootimg', flavor=flavor)
|
319
319
|
if not isinstance(file_list, list):
|
320
320
|
raise ValueError("Expected list of extracted files, got something else.")
|
321
|
+
|
322
|
+
image_file = next(
|
323
|
+
(f for f in file_list if f.endswith(".wic") or f.endswith(".img")),
|
324
|
+
None
|
325
|
+
)
|
321
326
|
|
322
|
-
|
323
|
-
|
324
|
-
raise FileNotFoundError("No .wic image file found after extraction.")
|
327
|
+
if not image_file:
|
328
|
+
raise FileNotFoundError("No .wic or .img image file found after extraction.")
|
325
329
|
|
326
330
|
except Exception as e:
|
327
331
|
raise RuntimeError(f"❌ Failed to download image: {e}")
|
328
332
|
|
329
333
|
try:
|
330
|
-
click.echo(f"📝 Writing image to removable media: {
|
331
|
-
write_bootimg(
|
334
|
+
click.echo(f"📝 Writing image to removable media: {image_file}")
|
335
|
+
write_bootimg(image_file)
|
336
|
+
|
332
337
|
except Exception as e:
|
333
338
|
raise RuntimeError(f"❌ Failed to write image: {e}")
|
334
339
|
|
sima_cli/update/query.py
CHANGED
@@ -5,17 +5,32 @@ from sima_cli.utils.config import get_auth_token
|
|
5
5
|
|
6
6
|
ARTIFACTORY_BASE_URL = artifactory_url() + '/artifactory'
|
7
7
|
|
8
|
-
def _list_available_firmware_versions_internal(board: str, match_keyword: str = None, flavor: str = 'headless'):
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
"
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
def _list_available_firmware_versions_internal(board: str, match_keyword: str = None, flavor: str = 'headless', swtype: str = 'yocto'):
|
9
|
+
if swtype == 'yocto':
|
10
|
+
fw_path = f"{board}"
|
11
|
+
aql_query = f"""
|
12
|
+
items.find({{
|
13
|
+
"repo": "soc-images",
|
14
|
+
"path": {{
|
15
|
+
"$match": "{fw_path}/*"
|
16
|
+
}},
|
17
|
+
"type": "folder"
|
18
|
+
}}).include("repo", "path", "name")
|
19
|
+
""".strip()
|
20
|
+
elif swtype == 'elxr':
|
21
|
+
fw_path = f"elxr/{board}"
|
22
|
+
aql_query = f"""
|
23
|
+
items.find({{
|
24
|
+
"repo": "soc-images",
|
25
|
+
"path": {{
|
26
|
+
"$match": "{fw_path}/*/artifacts/palette"
|
27
|
+
}},
|
28
|
+
"name": "modalix-tftp-boot.tar.gz",
|
29
|
+
"type": "file"
|
30
|
+
}}).include("repo", "path", "name")
|
31
|
+
""".strip()
|
32
|
+
else:
|
33
|
+
raise ValueError(f"Unsupported swtype: {swtype}")
|
19
34
|
|
20
35
|
aql_url = f"{ARTIFACTORY_BASE_URL}/api/search/aql"
|
21
36
|
headers = {
|
@@ -29,14 +44,18 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
29
44
|
|
30
45
|
results = response.json().get("results", [])
|
31
46
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
47
|
+
if swtype == 'yocto':
|
48
|
+
# Reconstruct full paths and remove board prefix
|
49
|
+
full_paths = {
|
50
|
+
f"{item['path']}/{item['name']}".replace(fw_path + "/", "")
|
51
|
+
for item in results
|
52
|
+
}
|
53
|
+
top_level_folders = sorted({path.split("/")[0] for path in full_paths})
|
54
|
+
else: # elxr
|
55
|
+
# Extract version from path like: elxr/{board}/<version>/artifacts/palette
|
56
|
+
top_level_folders = sorted({
|
57
|
+
item['path'].split('/')[2] for item in results
|
58
|
+
})
|
40
59
|
|
41
60
|
if match_keyword:
|
42
61
|
match_keyword = match_keyword.lower()
|
@@ -46,7 +65,8 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
46
65
|
|
47
66
|
return top_level_folders
|
48
67
|
|
49
|
-
|
68
|
+
|
69
|
+
def _list_available_firmware_versions_external(board: str, match_keyword: str = None, flavor: str = 'headless', swtype: str = 'yocto'):
|
50
70
|
"""
|
51
71
|
Construct and return a list containing a single firmware download URL for a given board.
|
52
72
|
|
@@ -57,11 +77,11 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
|
|
57
77
|
board (str): The name of the hardware board.
|
58
78
|
match_keyword (str, optional): A version string to match (e.g., '1.6' or '1.6.0').
|
59
79
|
flavor (str, optional): A string indicating firmware flavor - headless or full.
|
80
|
+
swtype (str, optional): A string indicating firmware type - yocto or elxr.
|
60
81
|
|
61
82
|
Returns:
|
62
83
|
list[str]: A list containing one formatted firmware download URL.
|
63
84
|
"""
|
64
|
-
fwtype = 'yocto'
|
65
85
|
cfg = load_resource_config()
|
66
86
|
download_url_base = cfg.get('public').get('download').get('download_url')
|
67
87
|
|
@@ -69,14 +89,17 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
|
|
69
89
|
if re.fullmatch(r'\d+\.\d+', match_keyword):
|
70
90
|
match_keyword += '.0'
|
71
91
|
|
92
|
+
# If it's headless then don't append flavor str to the URL, otherwise add it.
|
93
|
+
flavor_str = 'full-' if flavor == 'full' else ''
|
94
|
+
|
72
95
|
firmware_download_url = (
|
73
|
-
f'{download_url_base}SDK{match_keyword}/devkit/{board}/{
|
74
|
-
f'simaai-devkit-fw-{board}-{
|
96
|
+
f'{download_url_base}SDK{match_keyword}/devkit/{board}/{swtype}/'
|
97
|
+
f'simaai-devkit-fw-{board}-{swtype}-{flavor_str}{match_keyword}.tar.gz'
|
75
98
|
)
|
76
99
|
return [firmware_download_url]
|
77
100
|
|
78
101
|
|
79
|
-
def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False, flavor: str = 'headless'):
|
102
|
+
def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False, flavor: str = 'headless', swtype: str = 'yocto'):
|
80
103
|
"""
|
81
104
|
Public interface to list available firmware versions.
|
82
105
|
|
@@ -90,6 +113,6 @@ def list_available_firmware_versions(board: str, match_keyword: str = None, inte
|
|
90
113
|
- List[str] of firmware version folder names, or None if access is not allowed
|
91
114
|
"""
|
92
115
|
if not internal:
|
93
|
-
return _list_available_firmware_versions_external(board, match_keyword, flavor)
|
116
|
+
return _list_available_firmware_versions_external(board, match_keyword, flavor, swtype)
|
94
117
|
|
95
|
-
return _list_available_firmware_versions_internal(board, match_keyword, flavor)
|
118
|
+
return _list_available_firmware_versions_internal(board, match_keyword, flavor, swtype)
|
sima_cli/update/remote.py
CHANGED
@@ -237,13 +237,8 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
237
237
|
|
238
238
|
# Run Palette update
|
239
239
|
_flavor = 'palette' if flavor == 'headless' else 'graphics'
|
240
|
-
run_remote_command(
|
241
|
-
ssh,
|
242
|
-
f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i /tmp/{palette_name}",
|
243
|
-
password=passwd
|
244
|
-
)
|
245
|
-
click.echo("✅ Board image update complete.")
|
246
240
|
|
241
|
+
# Set necessary env first to make sure it can access NVMe device
|
247
242
|
if _flavor == 'graphics':
|
248
243
|
click.echo(f"⚠️ With full image, setting U-Boot environment variable to support NVMe and GPU.")
|
249
244
|
run_remote_command(
|
@@ -252,6 +247,13 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
252
247
|
password=passwd
|
253
248
|
)
|
254
249
|
|
250
|
+
run_remote_command(
|
251
|
+
ssh,
|
252
|
+
f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i /tmp/{palette_name}",
|
253
|
+
password=passwd
|
254
|
+
)
|
255
|
+
click.echo("✅ Board image update complete.")
|
256
|
+
|
255
257
|
# In the case of PCIe system, we don't need to reboot the card, instead, we will let it finish then update the PCIe driver in the host
|
256
258
|
# After that we can reboot the whole system.
|
257
259
|
if reboot_and_wait:
|
sima_cli/update/updater.py
CHANGED
@@ -29,7 +29,7 @@ else:
|
|
29
29
|
def convert_flavor(flavor: str = 'headless'):
|
30
30
|
return 'palette' if flavor == 'headless' else 'graphics'
|
31
31
|
|
32
|
-
def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = False, flavor: str = 'headless') -> str:
|
32
|
+
def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = False, flavor: str = 'headless', swtype: str = 'yocto') -> str:
|
33
33
|
"""
|
34
34
|
Resolve the final firmware download URL based on board, version, and environment.
|
35
35
|
|
@@ -38,6 +38,7 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
|
|
38
38
|
board (str): Board type ('davinci' or 'modalix').
|
39
39
|
internal (bool): Whether to use internal config for URL construction.
|
40
40
|
flavor (str): firmware image flavor, can be headless or full.
|
41
|
+
swtype (str): firmware image type, can be yocto or elxr
|
41
42
|
|
42
43
|
Returns:
|
43
44
|
str: Full download URL.
|
@@ -61,11 +62,16 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
|
|
61
62
|
if board == 'davinci':
|
62
63
|
flavor = 'headless'
|
63
64
|
|
64
|
-
|
65
|
-
|
65
|
+
if swtype == 'yocto':
|
66
|
+
image_file = 'release.tar.gz' if flavor == 'headless' else 'graphics.tar.gz'
|
67
|
+
download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/{image_file}"
|
68
|
+
elif swtype == 'elxr':
|
69
|
+
image_file = 'elxr-palette-modalix-1.7.0-arm64.img.gz' # temporary, this should change.
|
70
|
+
download_url = url.rstrip("/") + f"/soc-images/elxr/{board}/{version_or_url}/artifacts/palette/{image_file}"
|
71
|
+
|
66
72
|
return download_url
|
67
73
|
|
68
|
-
def _pick_from_available_versions(board: str, version_or_url: str, internal: bool, flavor: str) -> str:
|
74
|
+
def _pick_from_available_versions(board: str, version_or_url: str, internal: bool, flavor: str, swtype: str) -> str:
|
69
75
|
"""
|
70
76
|
Presents an interactive menu (with search) for selecting a firmware version.
|
71
77
|
"""
|
@@ -73,7 +79,7 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
|
|
73
79
|
if "http" in version_or_url:
|
74
80
|
return version_or_url
|
75
81
|
|
76
|
-
available_versions = list_available_firmware_versions(board, version_or_url, internal, flavor)
|
82
|
+
available_versions = list_available_firmware_versions(board, version_or_url, internal, flavor, swtype)
|
77
83
|
|
78
84
|
try:
|
79
85
|
if len(available_versions) > 1:
|
@@ -136,7 +142,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
136
142
|
|
137
143
|
Returns:
|
138
144
|
list: List of full paths to extracted files.
|
139
|
-
"""
|
145
|
+
"""
|
140
146
|
extract_dir = os.path.dirname(tar_path)
|
141
147
|
_flavor = convert_flavor(flavor)
|
142
148
|
|
@@ -166,8 +172,28 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
166
172
|
f"simaai-image-{_flavor}-{board}.wic.bmap",
|
167
173
|
f"simaai-image-{_flavor}-{board}.cpio.gz"
|
168
174
|
}
|
175
|
+
|
169
176
|
extracted_paths = []
|
170
177
|
|
178
|
+
# Handle .img.gz downloaded directly (e.g., ELXR palette image)
|
179
|
+
if tar_path.endswith(".img.gz"):
|
180
|
+
extract_dir = os.path.dirname(tar_path)
|
181
|
+
uncompressed_path = tar_path[:-3] # Remove .gz → .img
|
182
|
+
|
183
|
+
if os.path.exists(uncompressed_path):
|
184
|
+
click.echo(f"⚠️ Skipping decompression: {uncompressed_path} already exists")
|
185
|
+
return [tar_path, uncompressed_path]
|
186
|
+
|
187
|
+
try:
|
188
|
+
with gzip.open(tar_path, 'rb') as f_in:
|
189
|
+
with open(uncompressed_path, 'wb') as f_out:
|
190
|
+
shutil.copyfileobj(f_in, f_out)
|
191
|
+
click.echo(f"📦 Decompressed .img: {uncompressed_path}")
|
192
|
+
return [tar_path, uncompressed_path]
|
193
|
+
except Exception as e:
|
194
|
+
click.echo(f"❌ Failed to decompress {tar_path}: {e}")
|
195
|
+
return []
|
196
|
+
|
171
197
|
try:
|
172
198
|
try:
|
173
199
|
tar = tarfile.open(tar_path, mode="r:gz")
|
@@ -177,7 +203,8 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
177
203
|
with tar:
|
178
204
|
for member in tar.getmembers():
|
179
205
|
base_name = os.path.basename(member.name)
|
180
|
-
|
206
|
+
|
207
|
+
if base_name in target_filenames or base_name.endswith(".img.gz"):
|
181
208
|
full_dest_path = os.path.join(extract_dir, member.name)
|
182
209
|
|
183
210
|
if os.path.exists(full_dest_path):
|
@@ -189,9 +216,9 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
189
216
|
click.echo(f"✅ Extracted: {full_dest_path}")
|
190
217
|
extracted_paths.append(full_dest_path)
|
191
218
|
|
192
|
-
#
|
219
|
+
# Handle .wic.gz decompression
|
193
220
|
if full_dest_path.endswith(".wic.gz"):
|
194
|
-
uncompressed_path = full_dest_path[:-3]
|
221
|
+
uncompressed_path = full_dest_path[:-3]
|
195
222
|
|
196
223
|
if os.path.exists(uncompressed_path):
|
197
224
|
click.echo(f"⚠️ Skipping decompression: {uncompressed_path} already exists")
|
@@ -216,8 +243,9 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
|
|
216
243
|
click.echo(f"❌ Failed to extract files from archive: {e}")
|
217
244
|
return []
|
218
245
|
|
246
|
+
|
219
247
|
|
220
|
-
def _download_image(version_or_url: str, board: str, internal: bool = False, update_type: str = 'standard', flavor: str = 'headless'):
|
248
|
+
def _download_image(version_or_url: str, board: str, internal: bool = False, update_type: str = 'standard', flavor: str = 'headless', swtype: str = 'yocto'):
|
221
249
|
"""
|
222
250
|
Download or use a firmware image for the specified board and version or file path.
|
223
251
|
|
@@ -226,6 +254,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
|
|
226
254
|
board (str): Target board type ('davinci' or 'modalix').
|
227
255
|
internal (bool): Whether to use internal Artifactory resources.
|
228
256
|
flavor (str): Flavor of the image, can be headless or full, supported for Modalix only.
|
257
|
+
swtype (str): Type of the image, can be yocto or elxr, supported for all H/W platforms.
|
229
258
|
|
230
259
|
Notes:
|
231
260
|
- If a local file is provided, it skips downloading.
|
@@ -243,7 +272,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
|
|
243
272
|
image_url = version_or_url
|
244
273
|
else:
|
245
274
|
# Case 3: Resolve standard version string (Artifactory/AWS)
|
246
|
-
image_url = _resolve_firmware_url(version_or_url, board, internal, flavor=flavor)
|
275
|
+
image_url = _resolve_firmware_url(version_or_url, board, internal, flavor=flavor, swtype=swtype)
|
247
276
|
|
248
277
|
# Determine platform-safe temp directory
|
249
278
|
temp_dir = tempfile.gettempdir()
|
@@ -414,9 +443,9 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
|
|
414
443
|
"""
|
415
444
|
|
416
445
|
if 'http' not in version_or_url and not os.path.exists(version_or_url):
|
417
|
-
version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor)
|
446
|
+
version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor, swtype)
|
418
447
|
|
419
|
-
extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor)
|
448
|
+
extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor, swtype=swtype)
|
420
449
|
return extracted_paths
|
421
450
|
|
422
451
|
def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'headless'):
|
@@ -448,7 +477,7 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
|
|
448
477
|
board, version, fdt_name = get_remote_board_info(ip, passwd)
|
449
478
|
|
450
479
|
if board in ['davinci', 'modalix']:
|
451
|
-
click.echo(f"🔧 Target board: {board}, board currently running: {version}")
|
480
|
+
click.echo(f"🔧 Target board: {board} {fdt_name}, board currently running: {version}")
|
452
481
|
|
453
482
|
if flavor == 'full' and fdt_name != 'modalix-som.dtb':
|
454
483
|
click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
|
sima_cli/utils/disk.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
import shutil
|
2
|
+
|
3
|
+
def check_disk_space(required_bytes: int, folder: str = ".") -> bool:
|
4
|
+
"""
|
5
|
+
Check if the given folder has enough free disk space.
|
6
|
+
|
7
|
+
Args:
|
8
|
+
required_bytes (int): Space required in bytes
|
9
|
+
folder (str): Path to check (default: current dir)
|
10
|
+
|
11
|
+
Returns:
|
12
|
+
bool: True if enough space is available, False otherwise
|
13
|
+
"""
|
14
|
+
total, used, free = shutil.disk_usage(folder)
|
15
|
+
return free >= required_bytes
|
sima_cli/utils/env.py
CHANGED
@@ -92,6 +92,30 @@ def is_modalix_devkit() -> bool:
|
|
92
92
|
|
93
93
|
return False
|
94
94
|
|
95
|
+
def get_exact_devkit_type() -> str:
|
96
|
+
"""
|
97
|
+
Extracts the exact devkit type from 'fdt_name' in fw_printenv output.
|
98
|
+
|
99
|
+
Returns:
|
100
|
+
str: The value of fdt_name (e.g., "modalix-som"), or an empty string if not found or unavailable.
|
101
|
+
"""
|
102
|
+
if not shutil.which("fw_printenv"):
|
103
|
+
return ""
|
104
|
+
|
105
|
+
try:
|
106
|
+
output = subprocess.check_output(["fw_printenv"], text=True)
|
107
|
+
for line in output.splitlines():
|
108
|
+
line = line.strip()
|
109
|
+
if line.startswith("fdt_name="):
|
110
|
+
_, value = line.split("=", 1)
|
111
|
+
return value.strip().replace('.dtb','')
|
112
|
+
except subprocess.CalledProcessError:
|
113
|
+
return ""
|
114
|
+
except Exception:
|
115
|
+
return ""
|
116
|
+
|
117
|
+
return ""
|
118
|
+
|
95
119
|
def is_palette_sdk() -> bool:
|
96
120
|
"""
|
97
121
|
Check if the environment is running inside the Palette SDK container.
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: sima-cli
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.23
|
4
4
|
Summary: CLI tool for SiMa Developer Portal to download models, firmware, and apps.
|
5
5
|
Home-page: https://developer.sima.ai/
|
6
6
|
Author: SiMa.ai
|
@@ -26,6 +26,7 @@ Requires-Dist: rich
|
|
26
26
|
Requires-Dist: InquirerPy
|
27
27
|
Requires-Dist: tftpy
|
28
28
|
Requires-Dist: psutil
|
29
|
+
Requires-Dist: huggingface_hub
|
29
30
|
Dynamic: author
|
30
31
|
Dynamic: license-file
|
31
32
|
Dynamic: requires-python
|