sima-cli 0.0.19__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/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
- click.echo(f"Unable to retrieve board info {e}")
88
- return "", ""
97
+ click.echo(f"Unable to retrieve board info with error: {e}, board may be still booting.")
98
+ return "", "", ""
89
99
 
90
100
 
91
101
  def _scp_file(sftp, local_path: str, remote_path: str):
@@ -95,7 +105,7 @@ def _scp_file(sftp, local_path: str, remote_path: str):
95
105
  sftp.put(local_path, remote_path)
96
106
  click.echo("✅ Upload complete")
97
107
 
98
- def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
108
+ def run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
99
109
  """
100
110
  Run a remote command over SSH and stream its output live to the console.
101
111
  If the command starts with 'sudo', pipe in the password.
@@ -140,6 +150,13 @@ def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
140
150
  for line in remaining_err.splitlines():
141
151
  click.echo(f"⚠️ {line}")
142
152
 
153
+ def init_ssh_session(ip: str, password: str = DEFAULT_PASSWORD):
154
+ ssh = paramiko.SSHClient()
155
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
156
+
157
+ ssh.connect(ip, username=DEFAULT_USER, password=password, timeout=10)
158
+ return ssh
159
+
143
160
  def reboot_remote_board(ip: str, passwd: str):
144
161
  """
145
162
  Reboot remote board by sending SSH command
@@ -150,13 +167,37 @@ def reboot_remote_board(ip: str, passwd: str):
150
167
 
151
168
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
152
169
 
153
- _run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
154
- _run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
170
+ run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
171
+ run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
155
172
 
156
173
  except Exception as reboot_err:
157
174
  click.echo(f"⚠️ Unable to connect to the remote board")
158
175
 
159
- def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool):
176
+
177
+ def copy_file_to_remote_board(ip: str, file_path: str, remote_dir: str, passwd: str):
178
+ """
179
+ Copy a file to the remote board over SSH.
180
+ Assumes default credentials: sima / edgeai.
181
+ """
182
+ ssh = paramiko.SSHClient()
183
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
184
+
185
+ try:
186
+ ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
187
+ sftp = ssh.open_sftp()
188
+
189
+ # Upload the file
190
+ base_file_path = os.path.basename(file_path)
191
+ click.echo(f"📤 Uploading {file_path} → {remote_dir}")
192
+ sftp.put(file_path, os.path.join(remote_dir, base_file_path))
193
+ return True
194
+
195
+ except Exception as e:
196
+ click.echo(f"❌ Remote file copy failed: {e}")
197
+
198
+ return False
199
+
200
+ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool, flavor: str = 'headless'):
160
201
  """
161
202
  Upload and install firmware images to remote board over SSH.
162
203
  Assumes default credentials: sima / edgeai.
@@ -169,21 +210,24 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
169
210
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
170
211
  sftp = ssh.open_sftp()
171
212
  remote_dir = "/tmp"
172
-
173
- # Upload tRoot image
174
- troot_name = os.path.basename(troot_path)
175
213
  palette_name = os.path.basename(palette_path)
176
- _scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
177
- click.echo("🚀 Uploaded tRoot image.")
178
214
 
179
- # Run tRoot update
180
- _run_remote_command(
181
- ssh,
182
- f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
183
- )
184
- click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
185
- click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
186
- _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.")
187
231
 
188
232
  # Upload Palette image
189
233
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
@@ -192,13 +236,22 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
192
236
  click.echo("🚀 Uploaded system image.")
193
237
 
194
238
  # Run Palette update
195
- _run_remote_command(
239
+ _flavor = 'palette' if flavor == 'headless' else 'graphics'
240
+ run_remote_command(
196
241
  ssh,
197
- 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}",
198
243
  password=passwd
199
244
  )
200
245
  click.echo("✅ Board image update complete.")
201
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
+
202
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
203
256
  # After that we can reboot the whole system.
204
257
  if reboot_and_wait:
@@ -206,11 +259,11 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
206
259
  click.echo("🔁 Rebooting board after update. Waiting for reconnection...")
207
260
 
208
261
  try:
209
- _run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
210
- _run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
262
+ run_remote_command(ssh, "sudo reboot", password=passwd)
211
263
 
212
264
  except Exception as reboot_err:
213
265
  click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the board...")
266
+ click.confirm("⚠️ Have you powercycled the board?", default=True, abort=True)
214
267
 
215
268
  try:
216
269
  ssh.close()
@@ -229,7 +282,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
229
282
  ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
230
283
  ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
231
284
 
232
- _run_remote_command(ssh, "grep SIMA_BUILD_VERSION /etc/build 2>/dev/null || grep SIMA_BUILD_VERSION /etc/buildinfo 2>/dev/null", password=passwd)
285
+ run_remote_command(ssh, "grep SIMA_BUILD_VERSION /etc/build 2>/dev/null || grep SIMA_BUILD_VERSION /etc/buildinfo 2>/dev/null", password=passwd)
233
286
  ssh.close()
234
287
  except Exception as e:
235
288
  click.echo(f"❌ Unable to validate the version: {e}")
@@ -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,10 +151,21 @@ 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"
156
+ }
157
+ elif update_type == 'netboot':
158
+ target_filenames = {
159
+ "Image",
160
+ "netboot.scr.uimg",
161
+ f"{board}-hhhl.dtb",
162
+ f"{board}-som.dtb",
163
+ f"{board}-dvt.dtb",
164
+ f"{board}-hhhl_x16.dtb",
165
+ f"simaai-image-{_flavor}-{board}.wic.gz",
166
+ f"simaai-image-{_flavor}-{board}.wic.bmap",
167
+ f"simaai-image-{_flavor}-{board}.cpio.gz"
146
168
  }
147
-
148
169
  extracted_paths = []
149
170
 
150
171
  try:
@@ -196,7 +217,7 @@ def _extract_required_files(tar_path: str, board: str, update_type: str = 'stand
196
217
  return []
197
218
 
198
219
 
199
- 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'):
200
221
  """
201
222
  Download or use a firmware image for the specified board and version or file path.
202
223
 
@@ -204,6 +225,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
204
225
  version_or_url (str): Version string, HTTP(S) URL, or local file path.
205
226
  board (str): Target board type ('davinci' or 'modalix').
206
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.
207
229
 
208
230
  Notes:
209
231
  - If a local file is provided, it skips downloading.
@@ -214,14 +236,14 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
214
236
  # Case 1: Local file provided
215
237
  if os.path.exists(version_or_url) and os.path.isfile(version_or_url):
216
238
  click.echo(f"📁 Using local firmware file: {version_or_url}")
217
- return _extract_required_files(version_or_url, board, update_type)
239
+ return _extract_required_files(version_or_url, board, update_type, flavor)
218
240
 
219
241
  # Case 2: Treat as custom full URL
220
242
  if version_or_url.startswith("http://") or version_or_url.startswith("https://"):
221
243
  image_url = version_or_url
222
244
  else:
223
245
  # Case 3: Resolve standard version string (Artifactory/AWS)
224
- image_url = _resolve_firmware_url(version_or_url, board, internal)
246
+ image_url = _resolve_firmware_url(version_or_url, board, internal, flavor=flavor)
225
247
 
226
248
  # Determine platform-safe temp directory
227
249
  temp_dir = tempfile.gettempdir()
@@ -236,7 +258,7 @@ def _download_image(version_or_url: str, board: str, internal: bool = False, upd
236
258
  firmware_path = download_file_from_url(image_url, dest_path, internal=internal)
237
259
 
238
260
  click.echo(f"📦 Firmware downloaded to: {firmware_path}")
239
- return _extract_required_files(firmware_path, board, update_type)
261
+ return _extract_required_files(firmware_path, board, update_type, flavor)
240
262
 
241
263
  except Exception as e:
242
264
  click.echo(f"❌ Host update failed: {e}")
@@ -298,21 +320,26 @@ def _update_sdk(version_or_url: str, board: str):
298
320
  click.echo(f"⚙️ Simulated SDK firmware update logic for board '{board}' (not implemented).")
299
321
  # TODO: Implement update via SDK-based communication or tools
300
322
 
301
- 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):
302
324
  """
303
325
  Perform local firmware update using extracted files.
304
326
 
305
327
  Args:
306
328
  extracted_paths (List[str]): Paths to the extracted .swu files.
307
329
  board (str): Board type expected (e.g. 'davinci', 'modalix').
330
+ flavor (str): headless or full.
308
331
  """
309
332
  click.echo(f"⚙️ Starting local firmware update for board '{board}'...")
310
333
 
311
334
  # Locate the needed files
335
+ _flavor = 'palette' if flavor == 'headless' else 'graphics'
312
336
  troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
313
- 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)
338
+
339
+ if not troot_path:
340
+ click.echo("⚠️ tRoot update skipped because the requested image doesn't contain troot image.")
314
341
 
315
- if not troot_path or not palette_path:
342
+ if not palette_path:
316
343
  click.echo("❌ Required firmware files not found in extracted paths.")
317
344
  return
318
345
 
@@ -323,9 +350,9 @@ def _update_board(extracted_paths: List[str], board: str, passwd: str):
323
350
  return
324
351
 
325
352
  click.echo("✅ Board verified. Starting update...")
326
- push_and_update_local_board(troot_path, palette_path, passwd)
353
+ push_and_update_local_board(troot_path, palette_path, passwd, flavor)
327
354
 
328
- 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'):
329
356
  """
330
357
  Perform remote firmware update to the specified board via SSH.
331
358
 
@@ -334,21 +361,25 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
334
361
  ip (str): IP of the remote board.
335
362
  board (str): Expected board type ('davinci' or 'modalix').
336
363
  passwd (str): password to access the board, if it's not default
364
+ flavor (str): flavor of the firmware - headless or full
337
365
  """
338
366
  click.echo(f"⚙️ Starting remote update on '{ip}' for board type '{board}'...")
339
367
 
340
368
  # Locate files
369
+ _flavor = convert_flavor(flavor)
341
370
  troot_path = next((p for p in extracted_paths if "troot-upgrade" in os.path.basename(p)), None)
342
- 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)
343
372
  script_path = next((p for p in extracted_paths if p.endswith("sima_pcie_host_pkg.sh")), None)
344
373
 
345
- if not troot_path or not palette_path:
346
- 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.")
347
378
  return
348
379
 
349
380
  # Get remote board info
350
381
  click.echo("🔍 Checking remote board type and version...")
351
- remote_board, remote_version = get_remote_board_info(ip, passwd)
382
+ remote_board, remote_version, fdt_name = get_remote_board_info(ip, passwd)
352
383
 
353
384
  if not remote_board:
354
385
  click.echo("❌ Could not determine remote board type.")
@@ -361,12 +392,12 @@ def _update_remote(extracted_paths: List[str], ip: str, board: str, passwd: str,
361
392
  return
362
393
 
363
394
  # Proceed with update
364
- click.echo("✅ Board type verified. Proceeding with firmware update...")
365
- 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)
366
397
 
367
398
  return script_path
368
399
 
369
- 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'):
370
401
  """
371
402
  Download and extract a firmware image for a specified board.
372
403
 
@@ -376,18 +407,19 @@ def download_image(version_or_url: str, board: str, swtype: str, internal: bool
376
407
  swtype (str): The software type (default to 'davinci', possible values: `davinci`, `modalix`): not supported for now
377
408
  internal (bool): Whether to use internal download paths (e.g., Artifactory).
378
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.
379
411
 
380
412
  Returns:
381
413
  List[str]: Paths to the extracted image files.
382
414
  """
383
415
 
384
416
  if 'http' not in version_or_url and not os.path.exists(version_or_url):
385
- 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)
386
418
 
387
- 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)
388
420
  return extracted_paths
389
421
 
390
- 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'):
391
423
  r"""
392
424
  Update the system based on environment and input.
393
425
 
@@ -402,25 +434,35 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
402
434
  internal (bool): If True, enable internal-only behaviors (e.g., Artifactory access).
403
435
  passwd (str): Password for the board user (default: "edgeai").
404
436
  auto_confirm (bool): If True, auto-confirm firmware update without prompting.
437
+ flavor (str): headless or full
405
438
  """
406
439
  try:
407
440
  board = ''
408
441
  env_type, env_subtype = get_environment_type()
409
442
  click.echo(f"🔄 Running update for environment: {env_type} ({env_subtype})")
410
- click.echo(f"🔧 Requested version or URL: {version_or_url}")
443
+ click.echo(f"🔧 Requested version or URL: {version_or_url}, with flavor {flavor}")
411
444
 
412
445
  if env_type == 'board':
413
446
  board, version = get_local_board_info()
414
447
  else:
415
- board, version = get_remote_board_info(ip, passwd)
448
+ board, version, fdt_name = get_remote_board_info(ip, passwd)
416
449
 
417
450
  if board in ['davinci', 'modalix']:
418
451
  click.echo(f"🔧 Target board: {board}, board currently running: {version}")
419
-
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
+
420
462
  if 'http' not in version_or_url and not os.path.exists(version_or_url):
421
- 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)
422
464
 
423
- extracted_paths = _download_image(version_or_url, board, internal)
465
+ extracted_paths = _download_image(version_or_url, board, internal, flavor=flavor)
424
466
 
425
467
  if not auto_confirm:
426
468
  click.confirm(
@@ -435,15 +477,15 @@ def perform_update(version_or_url: str, ip: str = None, internal: bool = False,
435
477
  # Always update the remote device first then update the host driver, otherwise the host would
436
478
  # not be able to connect to the board
437
479
  click.echo("👉 Updating PCIe host driver and downloading firmware...")
438
- 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)
439
481
  _update_host(script_path, board, ip, passwd)
440
482
  elif env_type == "board":
441
- _update_board(extracted_paths, board, passwd)
483
+ _update_board(extracted_paths, board, passwd, flavor=flavor)
442
484
  elif env_type == "sdk":
443
485
  click.echo("👉 Updating firmware from within the Palette SDK...: Not implemented yet")
444
486
  elif ip:
445
487
  click.echo(f"👉 Updating firmware on remote board at {ip}...")
446
- _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)
447
489
  else:
448
490
  click.echo("❌ Unknown environment. Use --ip to specify target device.")
449
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.
sima_cli/utils/net.py ADDED
@@ -0,0 +1,29 @@
1
+ import psutil
2
+ import socket
3
+
4
+ def get_local_ip_candidates():
5
+ """
6
+ Return a list of IPv4 addresses on physical interfaces,
7
+ excluding VPN, loopback, and link-local interfaces.
8
+ """
9
+ vpn_prefixes = ("tun", "tap", "utun", "tailscale", "wg", "docker") # WireGuard, Tailscale, etc.
10
+ ip_list = []
11
+
12
+ for iface_name, iface_addrs in psutil.net_if_addrs().items():
13
+ # Exclude VPN or tunnel interfaces
14
+ if iface_name.startswith(vpn_prefixes):
15
+ continue
16
+
17
+ for addr in iface_addrs:
18
+ if addr.family == socket.AF_INET:
19
+ ip = addr.address
20
+
21
+ # Skip loopback and link-local
22
+ if ip.startswith("127.") or ip.startswith("169.254."):
23
+ continue
24
+
25
+ ip_list.append((iface_name, ip))
26
+
27
+ # Prioritize physical interfaces: eth0, en0, wlan0, etc.
28
+ ip_list.sort(key=lambda x: (not x[0].startswith(("eth", "en", "wlan", "wl")), x[0]))
29
+ return ip_list
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sima-cli
3
- Version: 0.0.19
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
@@ -24,6 +24,8 @@ Requires-Dist: paramiko
24
24
  Requires-Dist: plotext
25
25
  Requires-Dist: rich
26
26
  Requires-Dist: InquirerPy
27
+ Requires-Dist: tftpy
28
+ Requires-Dist: psutil
27
29
  Dynamic: author
28
30
  Dynamic: license-file
29
31
  Dynamic: requires-python