sima-cli 0.0.20__py3-none-any.whl → 0.0.22__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.
@@ -0,0 +1,208 @@
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 populate_resolv_conf(dns_server="8.8.8.8"):
116
+ """
117
+ Use sudo to write a DNS entry into /etc/resolv.conf even if not running as root.
118
+ """
119
+ content = f"nameserver {dns_server}\n"
120
+
121
+ try:
122
+ # Write using echo and sudo tee
123
+ cmd = f"echo '{content.strip()}' | sudo tee /etc/resolv.conf > /dev/null"
124
+ result = subprocess.run(cmd, shell=True, check=True)
125
+ print(f"✅ /etc/resolv.conf updated with nameserver {dns_server}")
126
+ except subprocess.CalledProcessError as e:
127
+ print(f"❌ Failed to update /etc/resolv.conf: {e}")
128
+
129
+ def set_default_route(iface, ip):
130
+ gateway = get_gateway_for_interface(ip)
131
+ if not gateway:
132
+ print(f"❌ Cannot set default route — IP not assigned for {iface}")
133
+ return
134
+
135
+ print(f"🔧 Setting default route via {iface} ({gateway})")
136
+
137
+ try:
138
+ # Delete all existing default routes
139
+ subprocess.run(["sudo", "/sbin/ip", "route", "del", "default"], check=False)
140
+
141
+ # Add new default route for this iface
142
+ subprocess.run(
143
+ ["sudo", "/sbin/ip", "route", "add", "default", "via", gateway, "dev", iface],
144
+ check=True
145
+ )
146
+ print(f"✅ Default route set via {iface} ({gateway})")
147
+ except subprocess.CalledProcessError:
148
+ print(f"❌ Failed to set default route via {iface}")
149
+
150
+ def network_menu():
151
+ if not is_sima_board():
152
+ print("❌ This command only runs on the DevKit")
153
+ return
154
+
155
+ while True:
156
+ interfaces = get_interfaces()
157
+ choices = ["🚪 Quit Menu"]
158
+ iface_map = {}
159
+
160
+ for iface in interfaces:
161
+ status_icon = "carrier (✅)" if iface["carrier"] else "carrier (❌)"
162
+ internet_icon = "internet (🌐)" if iface.get("internet") else "internet (🚫)"
163
+ label = f"{iface['name']:<10} {status_icon} {internet_icon} {iface['ip']:<20}"
164
+ choices.append(label)
165
+ iface_map[label] = iface
166
+
167
+ try:
168
+ iface_choice = inquirer.fuzzy(
169
+ message="Select Ethernet Interface:",
170
+ choices=choices,
171
+ instruction="(Type or use ↑↓)",
172
+ ).execute()
173
+ except KeyboardInterrupt:
174
+ print("\nExiting.")
175
+ break
176
+
177
+ if iface_choice is None or iface_choice == "🚪 Quit Menu":
178
+ print("Exiting.")
179
+ break
180
+
181
+ selected_iface = iface_map[iface_choice]
182
+
183
+ try:
184
+ second = inquirer.select(
185
+ message=f"Configure {selected_iface['name']}:",
186
+ choices=[
187
+ "Set to DHCP",
188
+ "Set to Default Static IP",
189
+ "Set as Default Route",
190
+ "Back to Interface Selection"
191
+ ]
192
+ ).execute()
193
+ except KeyboardInterrupt:
194
+ print("\nExiting.")
195
+ break
196
+
197
+ if second == "Set to DHCP":
198
+ move_network_file(selected_iface["name"], "dhcp")
199
+ populate_resolv_conf()
200
+ elif second == "Set to Default Static IP":
201
+ move_network_file(selected_iface["name"], "static")
202
+ elif second == "Set as Default Route":
203
+ set_default_route(selected_iface["name"], selected_iface["ip"])
204
+ else:
205
+ continue
206
+
207
+ if __name__ == '__main__':
208
+ network_menu()
sima_cli/nvme/nvme.py ADDED
@@ -0,0 +1,123 @@
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
+ try:
60
+ # Create mount point
61
+ subprocess.run("sudo mkdir -p /mnt/nvme", shell=True, check=True)
62
+
63
+ # Mount the NVMe partition
64
+ subprocess.run("sudo mount /dev/nvme0n1p1 /mnt/nvme", shell=True, check=True)
65
+
66
+ add_nvme_to_fstab()
67
+
68
+ subprocess.run("sudo mount -a", shell=True, check=True)
69
+
70
+ # Change ownership to user 'sima'
71
+ subprocess.run("sudo chown sima:sima /mnt/nvme", shell=True, check=True)
72
+
73
+ subprocess.run("sudo chmod 755 /mnt/nvme", shell=True, check=True)
74
+
75
+
76
+ print("✅ NVMe mounted and write permission granted to user 'sima'.")
77
+
78
+ except subprocess.CalledProcessError as e:
79
+ print(f"❌ Error during NVMe mount: {e}")
80
+
81
+ def nvme_format():
82
+ if not is_modalix_devkit():
83
+ click.echo("❌ This command can only be run on the Modalix DevKit.")
84
+ return
85
+
86
+ nvme_info = scan_nvme()
87
+ if not nvme_info:
88
+ click.echo("❌ No NVMe drive detected.")
89
+ return
90
+ click.echo(nvme_info)
91
+
92
+ lbaf_index = get_lba_format_index()
93
+ if lbaf_index is None:
94
+ click.echo("❌ Failed to detect LBA format index.")
95
+ return
96
+ click.echo(f"ℹ️ Detected LBA format index: {lbaf_index}")
97
+
98
+ if not click.confirm("⚠️ Are you sure you want to format /dev/nvme0n1? This will erase all data."):
99
+ click.echo("❌ Aborted by user.")
100
+ return
101
+
102
+ try:
103
+ # Unmount before formatting, ignore error if not mounted
104
+ subprocess.run("sudo umount /mnt/nvme", shell=True, check=False)
105
+
106
+ # Format and mount
107
+ format_nvme(lbaf_index)
108
+ mount_nvme()
109
+ click.echo("✅ NVMe drive formatted and mounted at /mnt/nvme.")
110
+ except subprocess.CalledProcessError:
111
+ click.echo("❌ Formatting process failed.")
112
+
113
+
114
+ def nvme_remount():
115
+ if not is_modalix_devkit():
116
+ click.echo("❌ This command can only be run on the Modalix DevKit.")
117
+ return
118
+
119
+ try:
120
+ mount_nvme()
121
+
122
+ except subprocess.CalledProcessError as e:
123
+ 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...")