sima-cli 0.0.20__py3-none-any.whl → 0.0.21__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 CHANGED
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "0.0.20"
2
+ __version__ = "0.0.21"
sima_cli/cli.py CHANGED
@@ -10,6 +10,8 @@ from sima_cli.utils.config import CONFIG_PATH
10
10
  from sima_cli.install.optiview import install_optiview
11
11
  from sima_cli.install.hostdriver import install_hostdriver
12
12
  from sima_cli.serial.serial import connect_serial
13
+ from sima_cli.nvme.nvme import nvme_format, nvme_remount
14
+ from sima_cli.network.network import network_menu
13
15
 
14
16
  # Entry point for the CLI tool using Click's command group decorator
15
17
  @click.group()
@@ -140,12 +142,13 @@ def download(ctx, url, dest):
140
142
  help="Skip confirmation after firmware file is downloaded."
141
143
  )
142
144
  @click.option(
143
- '--passwd',
145
+ '-p', '--passwd',
144
146
  default='edgeai',
145
147
  help="Optional SSH password for remote board (default is 'edgeai')."
146
148
  )
149
+ @click.option("-f", "--flavor", type=click.Choice(["headless", "full"], case_sensitive=False), default="headless", show_default=True, help="firmware flavor.")
147
150
  @click.pass_context
148
- def update(ctx, version_or_url, ip, yes, passwd):
151
+ def update(ctx, version_or_url, ip, yes, passwd, flavor):
149
152
  """
150
153
  Run system update across different environments.
151
154
  Downloads and applies firmware updates for PCIe host or SiMa board.
@@ -153,7 +156,7 @@ def update(ctx, version_or_url, ip, yes, passwd):
153
156
  version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package.
154
157
  """
155
158
  internal = ctx.obj.get("internal", False)
156
- perform_update(version_or_url, ip, internal, passwd=passwd, auto_confirm=yes)
159
+ perform_update(version_or_url, ip, internal, passwd=passwd, auto_confirm=yes, flavor=flavor)
157
160
 
158
161
  # ----------------------
159
162
  # Model Zoo Subcommands
@@ -226,18 +229,20 @@ def show_mla_memory_usage(ctx):
226
229
  # ----------------------
227
230
  @main.command(name="bootimg")
228
231
  @click.option("-v", "--version", required=True, help="Firmware version to download and write (e.g., 1.6.0)")
229
- @click.option("--boardtype", type=click.Choice(["modalix", "mlsoc"], case_sensitive=False), default="mlsoc", show_default=True, help="Target board type.")
230
- @click.option("--netboot", is_flag=True, default=False, show_default=True, help="Prepare image for network boot and launch TFTP server.")
231
- @click.option("--autoflash", is_flag=True, default=False, show_default=True, help="Net boot the DevKit and automatically flash the internal storage")
232
+ @click.option("-b", "--boardtype", type=click.Choice(["modalix", "mlsoc"], case_sensitive=False), default="mlsoc", show_default=True, help="Target board type.")
233
+ @click.option("-n", "--netboot", is_flag=True, default=False, show_default=True, help="Prepare image for network boot and launch TFTP server.")
234
+ @click.option("-a", "--autoflash", is_flag=True, default=False, show_default=True, help="Net boot the DevKit and automatically flash the internal storage - TBD")
232
235
  @click.pass_context
233
236
  def bootimg_cmd(ctx, version, boardtype, netboot, autoflash):
234
237
  """
235
238
  Download and burn a removable media or setup TFTP boot.
236
239
 
240
+ Only supports headless image
241
+
237
242
  Examples:
238
243
  sima-cli bootimg -v 1.6.0
239
244
  sima-cli bootimg -v 1.6.0 --boardtype mlsoc
240
- sima-cli bootimg -v 1.6.0 --boardtype modalix
245
+ sima-cli bootimg -v 1.6.0 --boardtype mlsoc
241
246
  sima-cli bootimg -v 1.6.0 --boardtype modalix --netboot
242
247
  sima-cli bootimg -v 1.6.0 --boardtype modalix --autoflash
243
248
  """
@@ -250,14 +255,15 @@ def bootimg_cmd(ctx, version, boardtype, netboot, autoflash):
250
255
  click.echo(f"📦 Preparing boot image:")
251
256
  click.echo(f" 🔹 Version : {version}")
252
257
  click.echo(f" 🔹 Board Type: {boardtype}")
258
+ click.echo(f" 🔹 F/W Flavor: headless")
253
259
 
254
260
  try:
255
261
  boardtype = boardtype if boardtype != 'mlsoc' else 'davinci'
256
262
  if netboot or autoflash:
257
- setup_netboot(version, boardtype, internal, autoflash)
263
+ setup_netboot(version, boardtype, internal, autoflash, flavor='headless')
258
264
  click.echo("✅ Netboot image prepared and TFTP server is running.")
259
265
  else:
260
- write_image(version, boardtype, 'yocto', internal)
266
+ write_image(version, boardtype, 'yocto', internal, flavor='headless')
261
267
  click.echo("✅ Boot image successfully written.")
262
268
  click.echo("✅ Boot image successfully written.")
263
269
  except Exception as e:
@@ -326,6 +332,59 @@ def serial_cmd(ctx, baud):
326
332
  - Windows: shows PuTTY/Tera Term setup instructions
327
333
  """
328
334
  connect_serial(ctx, baud)
335
+
336
+ # ----------------------
337
+ # Network Subcommands
338
+ # ----------------------
339
+ @main.command(name="network")
340
+ @click.pass_context
341
+ def network_cmd(ctx):
342
+ """
343
+ Setup Network IP address on the DevKit
344
+
345
+ This command only works on the DevKit. It allows user to switch between DHCP and Static (Default addresses) IP.
346
+
347
+ """
348
+ network_menu()
349
+
350
+ # ----------------------
351
+ # NVME Subcommands
352
+ # ----------------------
353
+ NVME_OPERATIONS = {"format", "remount"}
354
+ @main.command(name="nvme")
355
+ @click.argument("operation", type=click.Choice(NVME_OPERATIONS, case_sensitive=False))
356
+ @click.pass_context
357
+ def nvme_cmd(ctx, operation):
358
+ """
359
+ Perform NVMe operations on the Modalix DevKit.
360
+
361
+ Available operations:
362
+
363
+ format - Format the NVMe drive and mount it to /mnt/nvme
364
+
365
+ remount - Remount the existing NVMe partition to /mnt/nvme
366
+
367
+ Example:
368
+ sima-cli nvme format
369
+
370
+ sima-cli nvme remount
371
+ """
372
+ operation = operation.lower()
373
+
374
+ if operation == "format":
375
+ nvme_format()
376
+
377
+ elif operation == "remount":
378
+ try:
379
+ nvme_remount()
380
+ click.echo("✅ NVMe drive successfully remounted at /mnt/nvme.")
381
+ except Exception as e:
382
+ click.echo(f"❌ Failed to remount NVMe drive: {e}")
383
+
384
+ else:
385
+ click.echo(f"❌ Unsupported NVMe operation: {operation}")
386
+ ctx.exit(1)
387
+
329
388
  # ----------------------
330
389
  # App Zoo Subcommands
331
390
  # ----------------------
@@ -0,0 +1,193 @@
1
+ from InquirerPy import inquirer
2
+ import subprocess
3
+ import os
4
+ import re
5
+ import time
6
+ from sima_cli.utils.env import is_sima_board
7
+
8
+ IP_CMD = "/sbin/ip"
9
+
10
+ def extract_interface_index(name):
11
+ """Extract numeric index from interface name for sorting (e.g., end0 → 0)."""
12
+ match = re.search(r'(\d+)$', name)
13
+ return int(match.group(1)) if match else float('inf')
14
+
15
+ def get_interfaces():
16
+ interfaces = []
17
+ ip_output = subprocess.check_output([IP_CMD, '-o', 'link', 'show']).decode()
18
+ for line in ip_output.splitlines():
19
+ match = re.match(r'\d+: (\w+):', line)
20
+ if match:
21
+ iface = match.group(1)
22
+ if iface.startswith('lo'):
23
+ continue
24
+ try:
25
+ with open(f"/sys/class/net/{iface}/carrier") as f:
26
+ carrier = f.read().strip() == "1"
27
+ except FileNotFoundError:
28
+ carrier = False
29
+
30
+ try:
31
+ ip_addr = subprocess.check_output([IP_CMD, '-4', 'addr', 'show', iface]).decode()
32
+ ip_match = re.search(r'inet (\d+\.\d+\.\d+\.\d+)', ip_addr)
33
+ ip = ip_match.group(1) if ip_match else "IP Not Assigned"
34
+ except subprocess.CalledProcessError:
35
+ ip = "IP Not Assigned"
36
+
37
+ # Check internet connectivity only if carrier is up
38
+ internet = False
39
+ if carrier:
40
+ try:
41
+ result = subprocess.run(
42
+ ["ping", "-I", iface, "-c", "1", "-W", "1", "8.8.8.8"],
43
+ stdout=subprocess.DEVNULL,
44
+ stderr=subprocess.DEVNULL
45
+ )
46
+ internet = result.returncode == 0
47
+ except Exception:
48
+ internet = False
49
+
50
+ interfaces.append({
51
+ "name": iface,
52
+ "carrier": carrier,
53
+ "ip": ip,
54
+ "internet": internet
55
+ })
56
+
57
+ interfaces.sort(key=lambda x: extract_interface_index(x["name"]))
58
+ return interfaces
59
+
60
+ def move_network_file(iface, mode):
61
+ try:
62
+ networkd_dir = "/etc/systemd/network"
63
+ files = os.listdir(networkd_dir)
64
+
65
+ # Match any static file for this iface
66
+ pattern = re.compile(r"(\d+)-(%s)-static\.network" % re.escape(iface))
67
+ static_file = next((f for f in files if pattern.match(f)), None)
68
+ if not static_file:
69
+ print(f"⚠️ No static .network file found for {iface}")
70
+ return
71
+
72
+ src = os.path.join(networkd_dir, static_file)
73
+ desired_prefix = "02" if mode == "static" else "20"
74
+ dst_file = f"{desired_prefix}-{iface}-static.network"
75
+ dst = os.path.join(networkd_dir, dst_file)
76
+
77
+ if static_file == dst_file:
78
+ print(f"✅ Interface {iface} is already set to {mode.upper()}. No changes made.")
79
+ else:
80
+ print(f"🔧 Changing mode of {iface} to {mode.upper()}...")
81
+ subprocess.run(["sudo", "mv", src, dst], check=True)
82
+
83
+ # Modify content only if going to static
84
+ if mode == "static":
85
+ # Read as normal user
86
+ with open(dst, "r") as f:
87
+ lines = f.readlines()
88
+ cleaned = [line for line in lines if "KernelCommandLine=!netcfg=dhcp" not in line]
89
+
90
+ # Only write if change is needed
91
+ if len(cleaned) != len(lines):
92
+ temp_path = f"/tmp/{iface}-static.network"
93
+ with open(temp_path, "w") as tmpf:
94
+ tmpf.writelines(cleaned)
95
+ subprocess.run(["sudo", "cp", temp_path, dst], check=True)
96
+ os.remove(temp_path)
97
+ print(f"✂️ Removed KernelCommandLine override from {dst_file}")
98
+ else:
99
+ print(f"✅ No KernelCommandLine override found — file already clean.")
100
+
101
+ # Restart networkd
102
+ subprocess.run(["sudo", "systemctl", "restart", "systemd-networkd"])
103
+ time.sleep(2)
104
+ except Exception as e:
105
+ print(f"❌ Unable to change configuration, error: {e}")
106
+
107
+ def get_gateway_for_interface(ip):
108
+ """Guess the gateway from the IP address, assuming .1 is the router."""
109
+ if ip == "IP Not Assigned":
110
+ return None
111
+ parts = ip.split('.')
112
+ parts[-1] = "1"
113
+ return ".".join(parts)
114
+
115
+ def set_default_route(iface, ip):
116
+ gateway = get_gateway_for_interface(ip)
117
+ if not gateway:
118
+ print(f"❌ Cannot set default route — IP not assigned for {iface}")
119
+ return
120
+
121
+ print(f"🔧 Setting default route via {iface} ({gateway})")
122
+
123
+ try:
124
+ # Delete all existing default routes
125
+ subprocess.run(["sudo", "/sbin/ip", "route", "del", "default"], check=False)
126
+
127
+ # Add new default route for this iface
128
+ subprocess.run(
129
+ ["sudo", "/sbin/ip", "route", "add", "default", "via", gateway, "dev", iface],
130
+ check=True
131
+ )
132
+ print(f"✅ Default route set via {iface} ({gateway})")
133
+ except subprocess.CalledProcessError:
134
+ print(f"❌ Failed to set default route via {iface}")
135
+
136
+ def network_menu():
137
+ if not is_sima_board():
138
+ print("❌ This command only runs on the DevKit")
139
+ return
140
+
141
+ while True:
142
+ interfaces = get_interfaces()
143
+ choices = ["🚪 Quit Menu"]
144
+ iface_map = {}
145
+
146
+ for iface in interfaces:
147
+ status_icon = "carrier (✅)" if iface["carrier"] else "carrier (❌)"
148
+ internet_icon = "internet (🌐)" if iface.get("internet") else "internet (🚫)"
149
+ label = f"{iface['name']:<10} {status_icon} {internet_icon} {iface['ip']:<20}"
150
+ choices.append(label)
151
+ iface_map[label] = iface
152
+
153
+ try:
154
+ iface_choice = inquirer.fuzzy(
155
+ message="Select Ethernet Interface:",
156
+ choices=choices,
157
+ instruction="(Type or use ↑↓)",
158
+ ).execute()
159
+ except KeyboardInterrupt:
160
+ print("\nExiting.")
161
+ break
162
+
163
+ if iface_choice is None or iface_choice == "🚪 Quit Menu":
164
+ print("Exiting.")
165
+ break
166
+
167
+ selected_iface = iface_map[iface_choice]
168
+
169
+ try:
170
+ second = inquirer.select(
171
+ message=f"Configure {selected_iface['name']}:",
172
+ choices=[
173
+ "Set to DHCP",
174
+ "Set to Default Static IP",
175
+ "Set as Default Route",
176
+ "Back to Interface Selection"
177
+ ]
178
+ ).execute()
179
+ except KeyboardInterrupt:
180
+ print("\nExiting.")
181
+ break
182
+
183
+ if second == "Set to DHCP":
184
+ move_network_file(selected_iface["name"], "dhcp")
185
+ elif second == "Set to Default Static IP":
186
+ move_network_file(selected_iface["name"], "static")
187
+ elif second == "Set as Default Route":
188
+ set_default_route(selected_iface["name"], selected_iface["ip"])
189
+ else:
190
+ continue
191
+
192
+ if __name__ == '__main__':
193
+ network_menu()
sima_cli/nvme/nvme.py ADDED
@@ -0,0 +1,112 @@
1
+ import subprocess
2
+ import click
3
+ from sima_cli.utils.env import is_modalix_devkit
4
+
5
+ def scan_nvme():
6
+ try:
7
+ nvme_list = subprocess.check_output("sudo nvme list", shell=True, text=True).strip()
8
+ if "/dev/nvme0n1" in nvme_list:
9
+ return nvme_list
10
+ except subprocess.CalledProcessError:
11
+ pass
12
+ return None
13
+
14
+ def get_lba_format_index():
15
+ try:
16
+ lba_output = subprocess.check_output("sudo nvme id-ns -H /dev/nvme0n1 | grep 'Relative Performance'", shell=True, text=True)
17
+ lbaf_line = lba_output.strip().split(":")[0]
18
+ lbaf_index = lbaf_line.split()[-1]
19
+ return lbaf_index
20
+ except Exception:
21
+ return None
22
+
23
+ def format_nvme(lbaf_index):
24
+ cmds = [
25
+ f"sudo nvme format /dev/nvme0n1 --lbaf={lbaf_index}",
26
+ "sudo parted -a optimal /dev/nvme0n1 mklabel gpt",
27
+ "sudo parted -a optimal /dev/nvme0n1 mkpart primary ext4 0% 100%",
28
+ "sudo mkfs.ext4 /dev/nvme0n1p1",
29
+ "sudo nvme smart-log -H /dev/nvme0n1"
30
+ ]
31
+ for cmd in cmds:
32
+ subprocess.run(cmd, shell=True, check=True)
33
+
34
+ def add_nvme_to_fstab():
35
+ """
36
+ Add /dev/nvme0n1p1 to /etc/fstab for persistent mounting at /mnt/nvme.
37
+ Only appends if the entry does not already exist.
38
+ Requires root permission to modify /etc/fstab.
39
+ """
40
+ fstab_path = "/etc/fstab"
41
+ nvme_entry = "/dev/nvme0n1p1 /mnt/nvme ext4 defaults 0 2"
42
+
43
+ try:
44
+ # Check if the entry already exists
45
+ with open(fstab_path, "r") as f:
46
+ 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.")
49
+ return
50
+
51
+ # Append the entry as sudo
52
+ append_cmd = f"echo '{nvme_entry}' | sudo tee -a {fstab_path} > /dev/null"
53
+ subprocess.run(append_cmd, shell=True, check=True)
54
+ click.echo("✅ /etc/fstab updated to include NVMe auto-mount.")
55
+ except Exception as e:
56
+ click.echo(f"❌ Failed to update /etc/fstab: {e}")
57
+
58
+ 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)
61
+
62
+ def nvme_format():
63
+ if not is_modalix_devkit():
64
+ click.echo("❌ This command can only be run on the Modalix DevKit.")
65
+ return
66
+
67
+ nvme_info = scan_nvme()
68
+ if not nvme_info:
69
+ click.echo("❌ No NVMe drive detected.")
70
+ return
71
+ click.echo(nvme_info)
72
+
73
+ lbaf_index = get_lba_format_index()
74
+ if lbaf_index is None:
75
+ click.echo("❌ Failed to detect LBA format index.")
76
+ return
77
+ click.echo(f"ℹ️ Detected LBA format index: {lbaf_index}")
78
+
79
+ if not click.confirm("⚠️ Are you sure you want to format /dev/nvme0n1? This will erase all data."):
80
+ click.echo("❌ Aborted by user.")
81
+ return
82
+
83
+ try:
84
+ # Unmount before formatting, ignore error if not mounted
85
+ subprocess.run("sudo umount /mnt/nvme", shell=True, check=False)
86
+
87
+ # Format and mount
88
+ format_nvme(lbaf_index)
89
+ mount_nvme()
90
+ add_nvme_to_fstab()
91
+
92
+ click.echo("✅ NVMe drive formatted and mounted at /mnt/nvme.")
93
+ except subprocess.CalledProcessError:
94
+ click.echo("❌ Formatting process failed.")
95
+
96
+
97
+ def nvme_remount():
98
+ if not is_modalix_devkit():
99
+ click.echo("❌ This command can only be run on the Modalix DevKit.")
100
+ return
101
+
102
+ 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()
110
+
111
+ except subprocess.CalledProcessError as e:
112
+ raise RuntimeError(f"Failed to remount NVMe: {e}")
@@ -5,8 +5,8 @@ import sys
5
5
  import os
6
6
  import select
7
7
  import re
8
- import threading
9
- from tftpy import TftpServer
8
+
9
+ from sima_cli.update.updater import download_image
10
10
 
11
11
  try:
12
12
  from tqdm import tqdm
@@ -299,7 +299,7 @@ def write_bootimg(image_path):
299
299
  click.echo("ℹ️ Please manually eject the device.")
300
300
 
301
301
 
302
- def write_image(version: str, board: str, swtype: str, internal: bool = False):
302
+ def write_image(version: str, board: str, swtype: str, internal: bool = False, flavor: str = 'headless'):
303
303
  """
304
304
  Download and write a bootable firmware image to a removable storage device.
305
305
 
@@ -308,13 +308,14 @@ def write_image(version: str, board: str, swtype: str, internal: bool = False):
308
308
  board (str): Target board type, e.g., "modalix" or "mlsoc".
309
309
  swtype (str): Software image type, e.g., "yocto" or "exlr".
310
310
  internal (bool): Whether to use internal download sources. Defaults to False.
311
+ flavor (str): Flavor of the software package - can be either headless or full.
311
312
 
312
313
  Raises:
313
314
  RuntimeError: If the download or write process fails.
314
315
  """
315
316
  try:
316
317
  click.echo(f"⬇️ Downloading boot image for version: {version}, board: {board}, swtype: {swtype}")
317
- file_list = download_image(version, board, swtype, internal, update_type='bootimg')
318
+ file_list = download_image(version, board, swtype, internal, update_type='bootimg', flavor=flavor)
318
319
  if not isinstance(file_list, list):
319
320
  raise ValueError("Expected list of extracted files, got something else.")
320
321
 
sima_cli/update/local.py CHANGED
@@ -73,7 +73,7 @@ def get_local_board_info() -> Tuple[str, str]:
73
73
  return board_type, build_version
74
74
 
75
75
 
76
- def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str):
76
+ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str, flavor: str):
77
77
  """
78
78
  Perform local firmware update using swupdate commands.
79
79
  Calls swupdate directly on the provided file paths.
@@ -82,15 +82,17 @@ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str)
82
82
 
83
83
  try:
84
84
  # Run tRoot update
85
- click.echo("⚙️ Flashing tRoot image...")
86
- if not _run_local_cmd(f"sudo swupdate -H simaai-image-troot:1.0 -i {troot_path}", passwd):
87
- click.echo(" tRoot update failed.")
88
- return
89
- click.echo("✅ tRoot update completed.")
90
-
85
+ if troot_path != None:
86
+ click.echo("⚙️ Flashing tRoot image...")
87
+ if not _run_local_cmd(f"sudo swupdate -H simaai-image-troot:1.0 -i {troot_path}", passwd):
88
+ click.echo("❌ tRoot update failed.")
89
+ return
90
+ click.echo("✅ tRoot update completed.")
91
+
91
92
  # Run Palette update
92
93
  click.echo("⚙️ Flashing System image...")
93
- if not _run_local_cmd(f"sudo swupdate -H simaai-image-palette:1.0 -i {palette_path}", passwd):
94
+ _flavor = 'palette' if flavor == 'headless' else 'graphics'
95
+ if not _run_local_cmd(f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i {palette_path}", passwd):
94
96
  click.echo("❌ System image update failed.")
95
97
  return
96
98
  click.echo("✅ System image update completed. Please powercycle the device")
@@ -127,7 +127,7 @@ class ClientManager:
127
127
 
128
128
  while not self.shutdown_event.is_set():
129
129
  try:
130
- board_type, build_version = get_remote_board_info(ip)
130
+ board_type, build_version, _ = get_remote_board_info(ip)
131
131
 
132
132
  if board_type and build_version:
133
133
  with self.lock:
@@ -346,7 +346,7 @@ def run_cli(client_manager):
346
346
  click.echo("\n🛑 Exiting netboot session.")
347
347
  return True
348
348
 
349
- def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False):
349
+ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: bool = False, flavor: str = 'headless'):
350
350
  """
351
351
  Download and serve a bootable image for network boot over TFTP with client monitoring.
352
352
 
@@ -355,6 +355,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
355
355
  board (str): Target board type, e.g., "modalix" or "davinci".
356
356
  internal (bool): Whether to use internal download sources. Defaults to False.
357
357
  autoflash (bool): Whether to automatically flash the devkit when networked booted. Defaults to False.
358
+ flavor (str): The software flavor, can be either headless or full.
358
359
 
359
360
  Raises:
360
361
  RuntimeError: If the download or TFTP setup fails.
@@ -367,7 +368,7 @@ def setup_netboot(version: str, board: str, internal: bool = False, autoflash: b
367
368
 
368
369
  try:
369
370
  click.echo(f"⬇️ Downloading netboot image for version: {version}, board: {board}")
370
- file_list = download_image(version, board, swtype="yocto", internal=internal, update_type='netboot')
371
+ file_list = download_image(version, board, swtype="yocto", internal=internal, update_type='netboot', flavor=flavor)
371
372
  if not isinstance(file_list, list):
372
373
  raise ValueError("Expected list of extracted files, got something else.")
373
374
  extract_dir = os.path.dirname(file_list[0])
sima_cli/update/query.py CHANGED
@@ -5,7 +5,7 @@ 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):
8
+ def _list_available_firmware_versions_internal(board: str, match_keyword: str = None, flavor: str = 'headless'):
9
9
  fw_path = f"{board}"
10
10
  aql_query = f"""
11
11
  items.find({{
@@ -46,7 +46,7 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
46
46
 
47
47
  return top_level_folders
48
48
 
49
- def _list_available_firmware_versions_external(board: str, match_keyword: str = None):
49
+ def _list_available_firmware_versions_external(board: str, match_keyword: str = None, flavor: str = 'headless'):
50
50
  """
51
51
  Construct and return a list containing a single firmware download URL for a given board.
52
52
 
@@ -56,6 +56,7 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
56
56
  Args:
57
57
  board (str): The name of the hardware board.
58
58
  match_keyword (str, optional): A version string to match (e.g., '1.6' or '1.6.0').
59
+ flavor (str, optional): A string indicating firmware flavor - headless or full.
59
60
 
60
61
  Returns:
61
62
  list[str]: A list containing one formatted firmware download URL.
@@ -75,7 +76,7 @@ def _list_available_firmware_versions_external(board: str, match_keyword: str =
75
76
  return [firmware_download_url]
76
77
 
77
78
 
78
- def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False):
79
+ def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False, flavor: str = 'headless'):
79
80
  """
80
81
  Public interface to list available firmware versions.
81
82
 
@@ -83,11 +84,12 @@ def list_available_firmware_versions(board: str, match_keyword: str = None, inte
83
84
  - board: str – Name of the board (e.g. 'davinci')
84
85
  - match_keyword: str – Optional keyword to filter versions (case-insensitive)
85
86
  - internal: bool – Must be True to access internal Artifactory
87
+ - flavor (str, optional): A string indicating firmware flavor - headless or full.
86
88
 
87
89
  Returns:
88
90
  - List[str] of firmware version folder names, or None if access is not allowed
89
91
  """
90
92
  if not internal:
91
- return _list_available_firmware_versions_external(board, match_keyword)
93
+ return _list_available_firmware_versions_external(board, match_keyword, flavor)
92
94
 
93
- return _list_available_firmware_versions_internal(board, match_keyword)
95
+ return _list_available_firmware_versions_internal(board, match_keyword, flavor)
sima_cli/update/remote.py CHANGED
@@ -51,41 +51,51 @@ def _wait_for_ssh(ip: str, timeout: int = 120):
51
51
  else:
52
52
  print("\r✅ Board is online! \n")
53
53
 
54
- def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str]:
54
+ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str, str, str]:
55
55
  """
56
- Connect to the remote board and retrieve board type and build version.
56
+ Connect to the remote board and retrieve board type, build version, and fdt_name.
57
57
 
58
58
  Args:
59
59
  ip (str): IP address of the board.
60
60
 
61
61
  Returns:
62
- (board_type, build_version): Tuple of strings, or ('', '') on failure.
62
+ (board_type, build_version, fdt_name): Tuple of strings, or ('', '', '') on failure.
63
63
  """
64
64
  board_type = ""
65
65
  build_version = ""
66
+ fdt_name = ""
66
67
 
67
68
  try:
68
69
  ssh = paramiko.SSHClient()
69
70
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
70
71
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
71
72
 
72
- # Try /etc/build first then /etc/buildinfo
73
+ # Retrieve build info
73
74
  stdin, stdout, stderr = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
74
- output = stdout.read().decode()
75
+ build_output = stdout.read().decode()
76
+
77
+ # Retrieve fdt_name from fw_printenv
78
+ stdin, stdout, stderr = ssh.exec_command("fw_printenv fdt_name 2>/dev/null")
79
+ fdt_output = stdout.read().decode()
80
+
75
81
  ssh.close()
76
82
 
77
- for line in output.splitlines():
83
+ for line in build_output.splitlines():
78
84
  line = line.strip()
79
85
  if line.startswith("MACHINE"):
80
86
  board_type = line.split("=", 1)[-1].strip()
81
87
  elif line.startswith("SIMA_BUILD_VERSION"):
82
88
  build_version = line.split("=", 1)[-1].strip()
83
89
 
84
- return board_type, build_version
90
+ for line in fdt_output.splitlines():
91
+ if line.startswith("fdt_name"):
92
+ fdt_name = line.split("=", 1)[-1].strip()
93
+
94
+ return board_type, build_version, fdt_name
85
95
 
86
96
  except Exception as e:
87
97
  click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
88
- return "", ""
98
+ return "", "", ""
89
99
 
90
100
 
91
101
  def _scp_file(sftp, local_path: str, remote_path: str):
@@ -187,7 +197,7 @@ def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd:
187
197
 
188
198
  return False
189
199
 
190
- def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool):
200
+ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool, flavor: str = 'headless'):
191
201
  """
192
202
  Upload and install firmware images to remote board over SSH.
193
203
  Assumes default credentials: sima / edgeai.
@@ -200,21 +210,24 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
200
210
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
201
211
  sftp = ssh.open_sftp()
202
212
  remote_dir = "/tmp"
203
-
204
- # Upload tRoot image
205
- troot_name = os.path.basename(troot_path)
206
213
  palette_name = os.path.basename(palette_path)
207
- _scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
208
- click.echo("🚀 Uploaded tRoot image.")
209
214
 
210
- # Run tRoot update
211
- run_remote_command(
212
- ssh,
213
- f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
214
- )
215
- click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
216
- click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
217
- _wait_for_ssh(ip, timeout=120)
215
+ # Upload tRoot image
216
+ if troot_path is not None:
217
+ troot_name = os.path.basename(troot_path)
218
+ _scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
219
+ click.echo("🚀 Uploaded tRoot image.")
220
+
221
+ # Run tRoot update
222
+ run_remote_command(
223
+ ssh,
224
+ f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
225
+ )
226
+ click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
227
+ click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
228
+ _wait_for_ssh(ip, timeout=120)
229
+ else:
230
+ click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
218
231
 
219
232
  # Upload Palette image
220
233
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
@@ -223,13 +236,22 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
223
236
  click.echo("🚀 Uploaded system image.")
224
237
 
225
238
  # Run Palette update
239
+ _flavor = 'palette' if flavor == 'headless' else 'graphics'
226
240
  run_remote_command(
227
241
  ssh,
228
- f"sudo swupdate -H simaai-image-palette:1.0 -i /tmp/{palette_name}",
242
+ f"sudo swupdate -H simaai-image-{_flavor}:1.0 -i /tmp/{palette_name}",
229
243
  password=passwd
230
244
  )
231
245
  click.echo("✅ Board image update complete.")
232
246
 
247
+ if _flavor == 'graphics':
248
+ click.echo(f"⚠️ With full image, setting U-Boot environment variable to support NVMe and GPU.")
249
+ run_remote_command(
250
+ ssh,
251
+ f"sudo fw_setenv dtbos pcie-4rc-2rc-2rc.dtbo",
252
+ password=passwd
253
+ )
254
+
233
255
  # 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
234
256
  # After that we can reboot the whole system.
235
257
  if reboot_and_wait:
@@ -237,8 +259,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
237
259
  click.echo("🔁 Rebooting board after update. Waiting for reconnection...")
238
260
 
239
261
  try:
240
- run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
241
- run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
262
+ run_remote_command(ssh, "sudo reboot", password=passwd)
242
263
 
243
264
  except Exception as reboot_err:
244
265
  click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the board...")
@@ -26,7 +26,10 @@ else:
26
26
  push_and_update_local_board = None
27
27
 
28
28
 
29
- def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = False) -> str:
29
+ def convert_flavor(flavor: str = 'headless'):
30
+ return 'palette' if flavor == 'headless' else 'graphics'
31
+
32
+ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = False, flavor: str = 'headless') -> str:
30
33
  """
31
34
  Resolve the final firmware download URL based on board, version, and environment.
32
35
 
@@ -34,6 +37,7 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
34
37
  version_or_url (str): Either a version string (e.g. 1.6.0_master_B1611) or a full URL.
35
38
  board (str): Board type ('davinci' or 'modalix').
36
39
  internal (bool): Whether to use internal config for URL construction.
40
+ flavor (str): firmware image flavor, can be headless or full.
37
41
 
38
42
  Returns:
39
43
  str: Full download URL.
@@ -53,11 +57,15 @@ def _resolve_firmware_url(version_or_url: str, board: str, internal: bool = Fals
53
57
  if not url:
54
58
  raise RuntimeError("⚠️ 'url' is not defined in resource config.")
55
59
 
56
- # Format full download path, internal for now.
57
- download_url = url.rstrip("/") + f"/soc-images/{board}/{version_or_url}/artifacts/release.tar.gz"
60
+ # Davinci only supports headless images
61
+ if board == 'davinci':
62
+ flavor = 'headless'
63
+
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}"
58
66
  return download_url
59
67
 
60
- def _pick_from_available_versions(board: str, version_or_url: str, internal: bool) -> str:
68
+ def _pick_from_available_versions(board: str, version_or_url: str, internal: bool, flavor: str) -> str:
61
69
  """
62
70
  Presents an interactive menu (with search) for selecting a firmware version.
63
71
  """
@@ -65,7 +73,7 @@ def _pick_from_available_versions(board: str, version_or_url: str, internal: boo
65
73
  if "http" in version_or_url:
66
74
  return version_or_url
67
75
 
68
- available_versions = list_available_firmware_versions(board, version_or_url, internal)
76
+ available_versions = list_available_firmware_versions(board, version_or_url, internal, flavor)
69
77
 
70
78
  try:
71
79
  if len(available_versions) > 1:
@@ -113,7 +121,7 @@ def _sanitize_url_to_filename(url: str) -> str:
113
121
  return safe_name
114
122
 
115
123
 
116
- def _extract_required_files(tar_path: str, board: str, update_type: str = 'standard') -> list:
124
+ def _extract_required_files(tar_path: str, board: str, update_type: str = 'standard', flavor: str = 'headless') -> list:
117
125
  """
118
126
  Extract required files from a .tar.gz or .tar archive into the same folder
119
127
  and return the full paths to the extracted files (with subfolder if present).
@@ -124,15 +132,17 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
124
132
  tar_path (str): Path to the downloaded or provided firmware archive.
125
133
  board (str): Board type ('davinci' or 'modalix').
126
134
  update_type (str): Update type ('standard' or 'bootimg').
135
+ flavor (str): flavor of the firmware ('full' or 'headless').
127
136
 
128
137
  Returns:
129
138
  list: List of full paths to extracted files.
130
139
  """
131
140
  extract_dir = os.path.dirname(tar_path)
141
+ _flavor = convert_flavor(flavor)
132
142
 
133
143
  target_filenames = {
134
144
  "troot-upgrade-simaai-ev.swu",
135
- f"simaai-image-palette-upgrade-{board}.swu"
145
+ f"simaai-image-{_flavor}-upgrade-{board}.swu"
136
146
  }
137
147
 
138
148
  env_type, _os = get_environment_type()
@@ -141,8 +151,8 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
141
151
 
142
152
  if update_type == 'bootimg':
143
153
  target_filenames = {
144
- f"simaai-image-palette-{board}.wic.gz",
145
- f"simaai-image-palette-{board}.wic.bmap"
154
+ f"simaai-image-{_flavor}-{board}.wic.gz",
155
+ f"simaai-image-{_flavor}-{board}.wic.bmap"
146
156
  }
147
157
  elif update_type == 'netboot':
148
158
  target_filenames = {
@@ -152,9 +162,9 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
152
162
  f"{board}-som.dtb",
153
163
  f"{board}-dvt.dtb",
154
164
  f"{board}-hhhl_x16.dtb",
155
- f"simaai-image-palette-{board}.wic.gz",
156
- f"simaai-image-palette-{board}.wic.bmap",
157
- f"simaai-image-palette-{board}.cpio.gz"
165
+ f"simaai-image-{_flavor}-{board}.wic.gz",
166
+ f"simaai-image-{_flavor}-{board}.wic.bmap",
167
+ f"simaai-image-{_flavor}-{board}.cpio.gz"
158
168
  }
159
169
  extracted_paths = []
160
170
 
@@ -207,7 +217,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
207
217
  return []
208
218
 
209
219
 
210
- def _download_image(version_or_url: str, board: str, internal: bool = False, update_type: str = 'standard'):
220
+ def _download_image(version_or_url: str, board: str, internal: bool = False, update_type: str = 'standard', flavor: str = 'headless'):
211
221
  """
212
222
  Download or use a firmware image for the specified board and version or file path.
213
223
 
@@ -215,6 +225,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
215
225
  version_or_url (str): Version string, HTTP(S) URL, or local file path.
216
226
  board (str): Target board type ('davinci' or 'modalix').
217
227
  internal (bool): Whether to use internal Artifactory resources.
228
+ flavor (str): Flavor of the image, can be headless or full, supported for Modalix only.
218
229
 
219
230
  Notes:
220
231
  - If a local file is provided, it skips downloading.
@@ -225,14 +236,14 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
225
236
  # Case 1: Local file provided
226
237
  if os.path.exists(version_or_url) and os.path.isfile(version_or_url):
227
238
  click.echo(f"📁 Using local firmware file: {version_or_url}")
228
- return _extract_required_files(version_or_url, board, update_type)
239
+ return _extract_required_files(version_or_url, board, update_type, flavor)
229
240
 
230
241
  # Case 2: Treat as custom full URL
231
242
  if version_or_url.startswith("http://") or version_or_url.startswith("https://"):
232
243
  image_url = version_or_url
233
244
  else:
234
245
  # Case 3: Resolve standard version string (Artifactory/AWS)
235
- image_url = _resolve_firmware_url(version_or_url, board, internal)
246
+ image_url = _resolve_firmware_url(version_or_url, board, internal, flavor=flavor)
236
247
 
237
248
  # Determine platform-safe temp directory
238
249
  temp_dir = tempfile.gettempdir()
@@ -247,7 +258,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
247
258
  firmware_path = download_file_from_url(image_url, dest_path, internal=internal)
248
259
 
249
260
  click.echo(f"📦 Firmware downloaded to: {firmware_path}")
250
- return _extract_required_files(firmware_path, board, update_type)
261
+ return _extract_required_files(firmware_path, board, update_type, flavor)
251
262
 
252
263
  except Exception as e:
253
264
  click.echo(f"❌ Host update failed: {e}")
@@ -309,21 +320,26 @@ def _update_sdk(version_or_url: str, board: str):
309
320
  click.echo(f"⚙️ Simulated SDK firmware update logic for board '{board}' (not implemented).")
310
321
  # TODO: Implement update via SDK-based communication or tools
311
322
 
312
- def _update_board(extracted_paths: List[str], board: str, passwd: str):
323
+ def _update_board(extracted_paths: List[str], board: str, passwd: str, flavor: str):
313
324
  """
314
325
  Perform local firmware update using extracted files.
315
326
 
316
327
  Args:
317
328
  extracted_paths (List[str]): Paths to the extracted .swu files.
318
329
  board (str): Board type expected (e.g. 'davinci', 'modalix').
330
+ flavor (str): headless or full.
319
331
  """
320
332
  click.echo(f"⚙️ Starting local firmware update for board '{board}'...")
321
333
 
322
334
  # Locate the needed files
335
+ _flavor = 'palette' if flavor == 'headless' else 'graphics'
323
336
  troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
324
- palette_path = next((p for p in extracted_paths if f"palette-upgrade-{board}" in os.path.basename(p)), None)
337
+ palette_path = next((p for p in extracted_paths if f"{_flavor}-upgrade-{board}" in os.path.basename(p)), None)
325
338
 
326
- if not troot_path or not palette_path:
339
+ if not troot_path:
340
+ click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
341
+
342
+ if not palette_path:
327
343
  click.echo("❌ Required firmware files not found in extracted paths.")
328
344
  return
329
345
 
@@ -334,9 +350,9 @@ def _update_board(extracted_paths: List[str], board: str, passwd: str):
334
350
  return
335
351
 
336
352
  click.echo("✅ Board verified. Starting update...")
337
- push_and_update_local_board(troot_path, palette_path, passwd)
353
+ push_and_update_local_board(troot_path, palette_path, passwd, flavor)
338
354
 
339
- def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str, reboot_and_wait: bool = True):
355
+ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str, reboot_and_wait: bool = True, flavor: str = 'headless'):
340
356
  """
341
357
  Perform remote firmware update to the specified board via SSH.
342
358
 
@@ -345,21 +361,25 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
345
361
  ip (str): IP of the remote board.
346
362
  board (str): Expected board type ('davinci' or 'modalix').
347
363
  passwd (str): password to access the board, if it's not default
364
+ flavor (str): flavor of the firmware - headless or full
348
365
  """
349
366
  click.echo(f"⚙️ Starting remote update on '{ip}' for board type '{board}'...")
350
367
 
351
368
  # Locate files
369
+ _flavor = convert_flavor(flavor)
352
370
  troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
353
- palette_path = next((p for p in extracted_paths if f"palette-upgrade-{board}" in os.path.basename(p)), None)
371
+ palette_path = next((p for p in extracted_paths if f"{_flavor}-upgrade-{board}" in os.path.basename(p)), None)
354
372
  script_path = next((p for p in extracted_paths if p.endswith("sima_pcie_host_pkg.sh")), None)
355
373
 
356
- if not troot_path or not palette_path:
357
- click.echo("Required firmware files not found in extracted paths.")
374
+ if not troot_path:
375
+ click.echo("⚠️ Required troot firmware files not found in extracted paths, skipping tRoot update...")
376
+ if not palette_path:
377
+ click.echo("❌ Required o/s files not found in extracted paths.")
358
378
  return
359
379
 
360
380
  # Get remote board info
361
381
  click.echo("🔍 Checking remote board type and version...")
362
- remote_board, remote_version = get_remote_board_info(ip, passwd)
382
+ remote_board, remote_version, fdt_name = get_remote_board_info(ip, passwd)
363
383
 
364
384
  if not remote_board:
365
385
  click.echo("❌ Could not determine remote board type.")
@@ -372,12 +392,12 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
372
392
  return
373
393
 
374
394
  # Proceed with update
375
- click.echo("✅ Board type verified. Proceeding with firmware update...")
376
- push_and_update_remote_board(ip, troot_path, palette_path, passwd=passwd, reboot_and_wait=reboot_and_wait)
395
+ click.echo(f"✅ Board type verified. Proceeding with firmware update: troot : {troot_path}, os: {palette_path}...")
396
+ push_and_update_remote_board(ip, troot_path, palette_path, passwd=passwd, reboot_and_wait=reboot_and_wait, flavor=flavor)
377
397
 
378
398
  return script_path
379
399
 
380
- def download_image(version_or_url: str, board: str, swtype: str, internal: bool = False, update_type: str = 'standard'):
400
+ def download_image(version_or_url: str, board: str, swtype: str, internal: bool = False, update_type: str = 'standard', flavor: str = 'headless'):
381
401
  """
382
402
  Download and extract a firmware image for a specified board.
383
403
 
@@ -387,18 +407,19 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
387
407
  swtype (str): The software type (default to 'davinci', possible values: `davinci`, `modalix`): not supported for now
388
408
  internal (bool): Whether to use internal download paths (e.g., Artifactory).
389
409
  update_type (str): Whether this is standard update or writing boot image.
410
+ flavor (str): Flavor of the image, can be headless or full.
390
411
 
391
412
  Returns:
392
413
  List[str]: Paths to the extracted image files.
393
414
  """
394
415
 
395
416
  if 'http' not in version_or_url and not os.path.exists(version_or_url):
396
- version_or_url = _pick_from_available_versions(board, version_or_url, internal)
417
+ version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor)
397
418
 
398
- extracted_paths = _download_image(version_or_url, board, internal, update_type)
419
+ extracted_paths = _download_image(version_or_url, board, internal, update_type, flavor=flavor)
399
420
  return extracted_paths
400
421
 
401
- def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False):
422
+ def perform_update(version_or_url: str, ip: str = None, internal: bool = False, passwd: str = "edgeai", auto_confirm: bool = False, flavor: str = 'headless'):
402
423
  r"""
403
424
  Update the system based on environment and input.
404
425
 
@@ -413,25 +434,35 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
413
434
  internal (bool): If True, enable internal-only behaviors (e.g., Artifactory access).
414
435
  passwd (str): Password for the board user (default: "edgeai").
415
436
  auto_confirm (bool): If True, auto-confirm firmware update without prompting.
437
+ flavor (str): headless or full
416
438
  """
417
439
  try:
418
440
  board = ''
419
441
  env_type, env_subtype = get_environment_type()
420
442
  click.echo(f"🔄 Running update for environment: {env_type} ({env_subtype})")
421
- click.echo(f"🔧 Requested version or URL: {version_or_url}")
443
+ click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
422
444
 
423
445
  if env_type == 'board':
424
446
  board, version = get_local_board_info()
425
447
  else:
426
- board, version = get_remote_board_info(ip, passwd)
448
+ board, version, fdt_name = get_remote_board_info(ip, passwd)
427
449
 
428
450
  if board in ['davinci', 'modalix']:
429
451
  click.echo(f"🔧 Target board: {board}, board currently running: {version}")
430
-
452
+
453
+ if flavor == 'full' and fdt_name != 'modalix-som.dtb':
454
+ click.echo(f"❌ You've requested updating {fdt_name} to full image, this is only supported for the Modalix DevKit")
455
+ return
456
+
457
+ # Davinci only supports headless build, so ignore the full flavor
458
+ if board == 'davinci' and flavor != 'headless':
459
+ click.echo(f"MLSoC only supports headless image, ignoring {flavor} setting")
460
+ flavor = 'headless'
461
+
431
462
  if 'http' not in version_or_url and not os.path.exists(version_or_url):
432
- version_or_url = _pick_from_available_versions(board, version_or_url, internal)
463
+ version_or_url = _pick_from_available_versions(board, version_or_url, internal, flavor=flavor)
433
464
 
434
- extracted_paths = _download_image(version_or_url, board, internal)
465
+ extracted_paths = _download_image(version_or_url, board, internal, flavor=flavor)
435
466
 
436
467
  if not auto_confirm:
437
468
  click.confirm(
@@ -446,15 +477,15 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
446
477
  # Always update the remote device first then update the host driver, otherwise the host would
447
478
  # not be able to connect to the board
448
479
  click.echo("👉 Updating PCIe host driver and downloading firmware...")
449
- script_path = _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=False)
480
+ script_path = _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=False, flavor=flavor)
450
481
  _update_host(script_path, board, ip, passwd)
451
482
  elif env_type == "board":
452
- _update_board(extracted_paths, board, passwd)
483
+ _update_board(extracted_paths, board, passwd, flavor=flavor)
453
484
  elif env_type == "sdk":
454
485
  click.echo("👉 Updating firmware from within the Palette SDK...: Not implemented yet")
455
486
  elif ip:
456
487
  click.echo(f"👉 Updating firmware on remote board at {ip}...")
457
- _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=True)
488
+ _update_remote(extracted_paths, ip, board, passwd, reboot_and_wait=True, flavor=flavor)
458
489
  else:
459
490
  click.echo("❌ Unknown environment. Use --ip to specify target device.")
460
491
  else:
sima_cli/utils/env.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import subprocess
3
3
  import platform
4
+ import shutil
4
5
  from typing import Tuple
5
6
 
6
7
  # Utility functions to determine the environment:
@@ -70,6 +71,27 @@ def get_sima_board_type() -> str:
70
71
 
71
72
  return ""
72
73
 
74
+ def is_modalix_devkit() -> bool:
75
+ """
76
+ Determines whether the current system is a Modalix DevKit (SOM)
77
+ by checking if 'fdt_name=modalix-som.dtb' is present in fw_printenv output.
78
+
79
+ Returns:
80
+ bool: True if running on a Modalix DevKit, False otherwise.
81
+ """
82
+ if not shutil.which("fw_printenv"):
83
+ return False
84
+
85
+ try:
86
+ output = subprocess.check_output(["fw_printenv"], text=True)
87
+ for line in output.splitlines():
88
+ if line.strip().startswith("fdt_name="):
89
+ return "modalix-som.dtb" in line
90
+ except subprocess.CalledProcessError:
91
+ return False
92
+
93
+ return False
94
+
73
95
  def is_palette_sdk() -> bool:
74
96
  """
75
97
  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.20
3
+ Version: 0.0.21
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
@@ -1,7 +1,7 @@
1
1
  sima_cli/__init__.py,sha256=Nb2jSg9-CX1XvSc1c21U9qQ3atINxphuNkNfmR-9P3o,332
2
2
  sima_cli/__main__.py,sha256=ehzD6AZ7zGytC2gLSvaJatxeD0jJdaEvNJvwYeGsWOg,69
3
- sima_cli/__version__.py,sha256=U-M6HF-1PsXkez7yhqMRRsr6FObzLVRQ6Spmo4bkl30,49
4
- sima_cli/cli.py,sha256=b4ScjEa5Ltikry4ftZdJaNxBlSMYtmjYlodqzbRyWRk,12658
3
+ sima_cli/__version__.py,sha256=5C0RJVUsHkVkCFWA58oGCy10CpKRe_uE6H3nKgYCdRk,49
4
+ sima_cli/cli.py,sha256=OwoMg0UFl4Qy4FTCj5fXSNg-BjhC91dPYeZRSr15gBs,14447
5
5
  sima_cli/app_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  sima_cli/app_zoo/app.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sima_cli/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -18,26 +18,28 @@ sima_cli/install/palette.py,sha256=uRznoHa4Mv9ZXHp6AoqknfC3RxpYNKi9Ins756Cyifk,3
18
18
  sima_cli/mla/meminfo.py,sha256=ndc8kQJmWGEIdvNh6iIhATGdrkqM2pbddr_eHxaPNfg,1466
19
19
  sima_cli/model_zoo/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  sima_cli/model_zoo/model.py,sha256=q91Nrg62j1TqwPO8HiX4nlEFCCmzNEFcyFTBVMbJm8w,9836
21
+ sima_cli/network/network.py,sha256=C4vCBPT-n1SNdBDAhzFCawBhny0Y9g9yB2u2LlXVSG4,7012
22
+ sima_cli/nvme/nvme.py,sha256=ABLdRm83y2x-UX-rk8W7Uh2nvOnHYc6xECwLitRHcUc,3856
21
23
  sima_cli/sdk/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
24
  sima_cli/sdk/syscheck.py,sha256=h9zCULW67y4i2hqiGc-hc1ucBDShA5FAe9NxwBGq-fM,4575
23
25
  sima_cli/serial/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
26
  sima_cli/serial/serial.py,sha256=6xRta_PzE_DmmooYq35lbK76TYpAny5SEJAdYC_3fH0,4141
25
27
  sima_cli/update/__init__.py,sha256=0P-z-rSaev40IhfJXytK3AFWv2_sdQU4Ry6ei2sEus0,66
26
28
  sima_cli/update/bmaptool.py,sha256=KrhUGShBwY4Wzz50QiuMYAxxPgEy1nz5C68G-0a4qF4,4988
27
- sima_cli/update/bootimg.py,sha256=AOZI9bXrY8x-1gwlbIINdOUNO_naw9qteNJqeQ9_o-Y,13407
28
- sima_cli/update/local.py,sha256=CyUFLs5Lz5w4VyM6ip4wndKBBLz3_KZ-3scEvSiOrcg,3299
29
- sima_cli/update/netboot.py,sha256=xTtRf8LMuqC_Ye-m6tlv5kbwkZwexc623kRymwiLTf4,18528
30
- sima_cli/update/query.py,sha256=cVkUMLZkONJ2XMEwqEC-JqLVB38hOqfWM2hB2ehBK6Y,3272
31
- sima_cli/update/remote.py,sha256=RXQbNCDr7d8wLJ7pdGwA6G3gzgwrfZ9l_7YNYUfGHDU,10067
32
- sima_cli/update/updater.py,sha256=1K87YDj_isuMtL38ZI-hwBTIRmk3WqbkE6s-J0IVp-Q,18884
29
+ sima_cli/update/bootimg.py,sha256=jsxMv7OlrDP_fjzfTMn5UoiSOv7afB2LSM0pR50b4uE,13541
30
+ sima_cli/update/local.py,sha256=Blje7O2pcBopBLXwuVI826lnjPMTJ3lPU85dTUWUV48,3445
31
+ sima_cli/update/netboot.py,sha256=RqFgBhixcjPEwdVGvKhR0TeztoFnmGigmXlA71WVksA,18647
32
+ sima_cli/update/query.py,sha256=9yCW1ZQl42DAWV_7sbNsqEKeS9FzHdvgXpY5eS2GpDs,3540
33
+ sima_cli/update/remote.py,sha256=uv0cezLeG4tsJvalgm_VDOo3EUCU7LB3nXl8mNFFtds,10934
34
+ sima_cli/update/updater.py,sha256=gW6kIX0Xn16FWmaRryfu0BmM25bIaphCQ6tWu9N4tVY,20868
33
35
  sima_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
36
  sima_cli/utils/artifactory.py,sha256=6YyVpzVm8ATy7NEwT9nkWx-wptkXrvG7Wl_zDT6jmLs,2390
35
37
  sima_cli/utils/config.py,sha256=wE-cPQqY_gOqaP8t01xsRHD9tBUGk9MgBUm2GYYxI3E,1616
36
38
  sima_cli/utils/config_loader.py,sha256=7I5we1yiCai18j9R9jvhfUzAmT3OjAqVK35XSLuUw8c,2005
37
- sima_cli/utils/env.py,sha256=Jrb062EnpMBr1jGMotBlI2j9LEH6W1Z5Tgt6LHY7yYQ,5753
39
+ sima_cli/utils/env.py,sha256=m6yRnNuajYWTfysPJLk6vJY9Z1kYGFIAaArnpHXhino,6411
38
40
  sima_cli/utils/net.py,sha256=WVntA4CqipkNrrkA4tBVRadJft_pMcGYh4Re5xk3rqo,971
39
41
  sima_cli/utils/network.py,sha256=UvqxbqbWUczGFyO-t1SybG7Q-x9kjUVRNIn_D6APzy8,1252
40
- sima_cli-0.0.20.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
42
+ sima_cli-0.0.21.dist-info/licenses/LICENSE,sha256=a260OFuV4SsMZ6sQCkoYbtws_4o2deFtbnT9kg7Rfd4,1082
41
43
  tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
42
44
  tests/test_app_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
43
45
  tests/test_auth.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -46,8 +48,8 @@ tests/test_download.py,sha256=t87DwxlHs26_ws9rpcHGwr_OrcRPd3hz6Zmm0vRee2U,4465
46
48
  tests/test_firmware.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
47
49
  tests/test_model_zoo.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
48
50
  tests/test_utils.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
49
- sima_cli-0.0.20.dist-info/METADATA,sha256=0KwVrHOfciP7OQWLcpOhnarYPVPkTcffT4QSKNXqzwo,3674
50
- sima_cli-0.0.20.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
51
- sima_cli-0.0.20.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
52
- sima_cli-0.0.20.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
53
- sima_cli-0.0.20.dist-info/RECORD,,
51
+ sima_cli-0.0.21.dist-info/METADATA,sha256=vX4CMPMiH5QqByXV6CphuCM2rh1vwefsLRCctFcmP9o,3674
52
+ sima_cli-0.0.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ sima_cli-0.0.21.dist-info/entry_points.txt,sha256=xRYrDq1nCs6R8wEdB3c1kKuimxEjWJkHuCzArQPT0Xk,47
54
+ sima_cli-0.0.21.dist-info/top_level.txt,sha256=FtrbAUdHNohtEPteOblArxQNwoX9_t8qJQd59fagDlc,15
55
+ sima_cli-0.0.21.dist-info/RECORD,,