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.
@@ -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
- # Check if the entry already exists
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 "/dev/nvme0n1p1" in line or "/mnt/nvme" in line:
48
- click.echo("ℹ️ NVMe mount entry already exists in /etc/fstab.")
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 as sudo
52
- append_cmd = f"echo '{nvme_entry}' | sudo tee -a {fstab_path} > /dev/null"
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("✅ /etc/fstab updated to include NVMe auto-mount.")
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"❌ Failed to update /etc/fstab: {e}")
76
+ click.echo(f"❌ Unexpected error: {e}")
57
77
 
58
78
  def mount_nvme():
59
- subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
60
- subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
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
- # Ensure mount point exists
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)
@@ -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 "exlr".
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
- wic_file = next((f for f in file_list if f.endswith(".wic")), None)
323
- if not wic_file:
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: {wic_file}")
331
- write_bootimg(wic_file)
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
- fw_path = f"{board}"
10
- aql_query = f"""
11
- items.find({{
12
- "repo": "soc-images",
13
- "path": {{
14
- "$match": "{fw_path}/*"
15
- }},
16
- "type": "folder"
17
- }}).include("repo", "path", "name")
18
- """.strip()
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
- # Reconstruct full paths and remove board prefix
33
- full_paths = {
34
- f"{item['path']}/{item['name']}".replace(fw_path + "/", "")
35
- for item in results
36
- }
37
-
38
- # Extract top-level folders
39
- top_level_folders = sorted({path.split("/")[0] for path in full_paths})
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
- def _list_available_firmware_versions_external(board: str, match_keyword: str = None, flavor: str = 'headless'):
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}/{fwtype}/'
74
- f'simaai-devkit-fw-{board}-{fwtype}-{match_keyword}.tar.gz'
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:
@@ -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
- image_file = 'release.tar.gz' if flavor == 'headless' else 'graphics.tar.gz'
65
- download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/{image_file}"
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
- if base_name in target_filenames:
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
- # If it's a .wic.gz file, decompress it now
219
+ # Handle .wic.gz decompression
193
220
  if full_dest_path.endswith(".wic.gz"):
194
- uncompressed_path = full_dest_path[:-3] # remove .gz
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.21
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