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 +1 -1
- sima_cli/cli.py +68 -9
- sima_cli/network/network.py +193 -0
- sima_cli/nvme/nvme.py +112 -0
- sima_cli/update/bootimg.py +5 -4
- sima_cli/update/local.py +10 -8
- sima_cli/update/netboot.py +4 -3
- sima_cli/update/query.py +7 -5
- sima_cli/update/remote.py +46 -25
- sima_cli/update/updater.py +70 -39
- sima_cli/utils/env.py +22 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/METADATA +1 -1
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/RECORD +17 -15
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.20.dist-info → sima_cli-0.0.21.dist-info}/top_level.txt +0 -0
sima_cli/__version__.py
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
# sima_cli/__version__.py
|
2
|
-
__version__ = "0.0.
|
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",
|
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
|
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}")
|
sima_cli/update/bootimg.py
CHANGED
@@ -5,8 +5,8 @@ import sys
|
|
5
5
|
import os
|
6
6
|
import select
|
7
7
|
import re
|
8
|
-
|
9
|
-
from
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
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")
|
sima_cli/update/netboot.py
CHANGED
@@ -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
|
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
|
-
#
|
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
|
-
|
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
|
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
|
-
|
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
|
-
#
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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-
|
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
|
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...")
|
sima_cli/update/updater.py
CHANGED
@@ -26,7 +26,10 @@ else:
|
|
26
26
|
push_and_update_local_board = None
|
27
27
|
|
28
28
|
|
29
|
-
def
|
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
|
-
#
|
57
|
-
|
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-
|
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-
|
145
|
-
f"simaai-image-
|
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-
|
156
|
-
f"simaai-image-
|
157
|
-
f"simaai-image-
|
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"
|
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
|
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"
|
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
|
357
|
-
click.echo("
|
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,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=
|
4
|
-
sima_cli/cli.py,sha256=
|
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=
|
28
|
-
sima_cli/update/local.py,sha256=
|
29
|
-
sima_cli/update/netboot.py,sha256=
|
30
|
-
sima_cli/update/query.py,sha256=
|
31
|
-
sima_cli/update/remote.py,sha256=
|
32
|
-
sima_cli/update/updater.py,sha256=
|
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=
|
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.
|
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.
|
50
|
-
sima_cli-0.0.
|
51
|
-
sima_cli-0.0.
|
52
|
-
sima_cli-0.0.
|
53
|
-
sima_cli-0.0.
|
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,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|