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/__version__.py +1 -1
- sima_cli/cli.py +100 -12
- sima_cli/install/optiview.py +8 -3
- sima_cli/network/network.py +193 -0
- sima_cli/nvme/nvme.py +112 -0
- sima_cli/serial/__init__.py +0 -0
- sima_cli/serial/serial.py +114 -0
- sima_cli/update/bootimg.py +7 -6
- sima_cli/update/local.py +10 -8
- sima_cli/update/netboot.py +409 -0
- sima_cli/update/query.py +7 -5
- sima_cli/update/remote.py +84 -31
- sima_cli/update/updater.py +79 -37
- sima_cli/utils/env.py +22 -0
- sima_cli/utils/net.py +29 -0
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/METADATA +3 -1
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/RECORD +21 -15
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.19.dist-info → sima_cli-0.0.21.dist-info}/top_level.txt +0 -0
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
|
-
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
|
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
|
-
|
154
|
-
|
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
|
-
|
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
|
-
#
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
239
|
+
_flavor = 'palette' if flavor == 'headless' else 'graphics'
|
240
|
+
run_remote_command(
|
196
241
|
ssh,
|
197
|
-
f"sudo swupdate -H simaai-image-
|
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
|
-
|
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
|
-
|
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}")
|
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,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-
|
145
|
-
f"simaai-image-
|
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"
|
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
|
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"
|
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
|
346
|
-
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.")
|
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.
|
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
|