sima-cli 0.0.17__py3-none-any.whl → 0.0.19__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/auth/login.py +2 -2
- sima_cli/cli.py +81 -4
- sima_cli/data/resources_public.yaml +1 -1
- sima_cli/download/downloader.py +1 -0
- sima_cli/install/__init__.py +0 -0
- sima_cli/install/hostdriver.py +152 -0
- sima_cli/install/optiview.py +80 -0
- sima_cli/install/palette.py +87 -0
- sima_cli/update/bmaptool.py +137 -0
- sima_cli/update/bootimg.py +339 -0
- sima_cli/update/local.py +19 -14
- sima_cli/update/query.py +30 -1
- sima_cli/update/remote.py +16 -10
- sima_cli/update/updater.py +96 -35
- sima_cli/utils/env.py +33 -29
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/METADATA +1 -1
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/RECORD +22 -16
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/WHEEL +0 -0
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/entry_points.txt +0 -0
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/licenses/LICENSE +0 -0
- {sima_cli-0.0.17.dist-info → sima_cli-0.0.19.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,339 @@
|
|
1
|
+
import click
|
2
|
+
import platform
|
3
|
+
import subprocess
|
4
|
+
import sys
|
5
|
+
import os
|
6
|
+
import select
|
7
|
+
import time
|
8
|
+
import re
|
9
|
+
try:
|
10
|
+
from tqdm import tqdm
|
11
|
+
except ImportError:
|
12
|
+
tqdm = None
|
13
|
+
from sima_cli.update.updater import download_image
|
14
|
+
|
15
|
+
|
16
|
+
def list_removable_devices():
|
17
|
+
system = platform.system()
|
18
|
+
|
19
|
+
if system == "Linux":
|
20
|
+
return get_linux_removable()
|
21
|
+
elif system == "Darwin":
|
22
|
+
return get_macos_removable()
|
23
|
+
elif system == "Windows":
|
24
|
+
return get_windows_removable()
|
25
|
+
else:
|
26
|
+
click.echo(f"❌ Unsupported platform: {system}")
|
27
|
+
return []
|
28
|
+
|
29
|
+
# Linux: Use lsblk to find removable drives
|
30
|
+
def get_linux_removable():
|
31
|
+
try:
|
32
|
+
output = subprocess.check_output(["lsblk", "-o", "NAME,RM,SIZE,MOUNTPOINT", "-J"]).decode()
|
33
|
+
import json
|
34
|
+
data = json.loads(output)
|
35
|
+
devices = []
|
36
|
+
for block in data['blockdevices']:
|
37
|
+
if block.get('rm') == True and block.get('mountpoint') is None:
|
38
|
+
devices.append({
|
39
|
+
"name": block['name'],
|
40
|
+
"size": block['size'],
|
41
|
+
"path": f"/dev/{block['name']}"
|
42
|
+
})
|
43
|
+
return devices
|
44
|
+
except Exception:
|
45
|
+
return []
|
46
|
+
|
47
|
+
# macOS: Use diskutil
|
48
|
+
def get_macos_removable():
|
49
|
+
try:
|
50
|
+
output = subprocess.check_output(["diskutil", "list"], text=True)
|
51
|
+
devices = []
|
52
|
+
|
53
|
+
candidate_disks = [
|
54
|
+
line.split()[0]
|
55
|
+
for line in output.splitlines()
|
56
|
+
if line.startswith("/dev/disk")
|
57
|
+
]
|
58
|
+
|
59
|
+
for disk in candidate_disks:
|
60
|
+
info = subprocess.check_output(["diskutil", "info", disk], text=True)
|
61
|
+
is_removable = False
|
62
|
+
is_disk_image = False
|
63
|
+
size = "Unknown"
|
64
|
+
device_name = ''
|
65
|
+
|
66
|
+
for info_line in info.splitlines():
|
67
|
+
if "Secure Digital" in info_line or "USB" in info_line:
|
68
|
+
is_removable = True
|
69
|
+
elif "Disk Size" in info_line and "(" in info_line:
|
70
|
+
size = info_line.split("(")[1].split()[0]
|
71
|
+
elif "Volume Name" in info_line:
|
72
|
+
volume_name = info_line.split(":")[-1].strip() or "Unknown"
|
73
|
+
elif "Device / Media Name" in info_line:
|
74
|
+
is_disk_image = ('Disk Image' in info_line)
|
75
|
+
device_name = info_line.split(":")[-1].strip() or "Unknown"
|
76
|
+
|
77
|
+
# switch to raw device to speed up dd performance
|
78
|
+
if is_removable and not is_disk_image:
|
79
|
+
devices.append({
|
80
|
+
"name": device_name,
|
81
|
+
"size": round(int(size) / (1024 ** 3), 0),
|
82
|
+
"path": disk.replace('/disk', '/rdisk')
|
83
|
+
})
|
84
|
+
|
85
|
+
return devices
|
86
|
+
except Exception as e:
|
87
|
+
click.echo(f"Failed to detect removable devices on macOS: {e}")
|
88
|
+
return []
|
89
|
+
|
90
|
+
# Windows: Use wmic or powershell
|
91
|
+
def get_windows_removable():
|
92
|
+
try:
|
93
|
+
output = subprocess.check_output(
|
94
|
+
['powershell', '-Command',
|
95
|
+
'Get-WmiObject Win32_DiskDrive | Where { $_.MediaType -match "Removable" } | '
|
96
|
+
'Select-Object DeviceID,Model,Size | ConvertTo-Json']
|
97
|
+
).decode()
|
98
|
+
import json
|
99
|
+
parsed = json.loads(output)
|
100
|
+
if not isinstance(parsed, list):
|
101
|
+
parsed = [parsed]
|
102
|
+
devices = []
|
103
|
+
for d in parsed:
|
104
|
+
size_gb = int(d.get("Size", 0)) // (1024 ** 3)
|
105
|
+
devices.append({
|
106
|
+
"name": d.get("Model", "Removable Drive"),
|
107
|
+
"size": f"{size_gb} GB",
|
108
|
+
"path": d["DeviceID"]
|
109
|
+
})
|
110
|
+
return devices
|
111
|
+
except Exception:
|
112
|
+
return []
|
113
|
+
|
114
|
+
def check_dd_installed():
|
115
|
+
"""Check if dd is installed on the system."""
|
116
|
+
try:
|
117
|
+
subprocess.run(["which", "dd"], capture_output=True, check=True)
|
118
|
+
return True
|
119
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
120
|
+
return False
|
121
|
+
|
122
|
+
def unmount_device(device_path):
|
123
|
+
"""Unmount the device using platform-specific commands."""
|
124
|
+
system = platform.system()
|
125
|
+
try:
|
126
|
+
if system == "Darwin": # macOS
|
127
|
+
subprocess.run(["diskutil", "unmountDisk", device_path], check=True, capture_output=True, text=True)
|
128
|
+
click.echo(f"✅ Unmounted {device_path} on macOS")
|
129
|
+
elif system == "Linux":
|
130
|
+
result = subprocess.run(["umount", device_path], capture_output=True, text=True)
|
131
|
+
if result.returncode == 0:
|
132
|
+
click.echo(f"✅ Unmounted {device_path} on Linux")
|
133
|
+
elif "not mounted" in result.stderr.lower():
|
134
|
+
click.echo(f"ℹ️ {device_path} was not mounted. Continuing.")
|
135
|
+
else:
|
136
|
+
click.echo(f"❌ Failed to unmount {device_path}: {result.stderr.strip()}")
|
137
|
+
sys.exit(1)
|
138
|
+
else:
|
139
|
+
click.echo(f"❌ Unsupported platform: {system}. Cannot unmount {device_path}.")
|
140
|
+
sys.exit(1)
|
141
|
+
except Exception as e:
|
142
|
+
click.echo(f"❌ Unexpected error while unmounting {device_path}: {e}")
|
143
|
+
sys.exit(1)
|
144
|
+
|
145
|
+
def _require_sudo():
|
146
|
+
try:
|
147
|
+
# This will prompt for password if necessary and cache it for a few minutes
|
148
|
+
click.echo("✅ Running this command requires sudo access.")
|
149
|
+
subprocess.run(["sudo", "-v"], check=True)
|
150
|
+
except subprocess.CalledProcessError:
|
151
|
+
click.echo("❌ Sudo authentication failed.")
|
152
|
+
sys.exit(1)
|
153
|
+
|
154
|
+
def copy_image_to_device(image_path, device_path):
|
155
|
+
"""Copy the image file to the device using dd with 16M block size."""
|
156
|
+
# Get file size for progress calculation
|
157
|
+
file_size = os.path.getsize(image_path)
|
158
|
+
click.echo(f"ℹ️ Running 'sudo dd' to copy {image_path} to {device_path}")
|
159
|
+
|
160
|
+
# Debug: Log raw dd output to a file for diagnosis
|
161
|
+
debug_log = "dd_output.log"
|
162
|
+
click.echo(f"ℹ️ Logging raw dd output to {debug_log} for debugging.")
|
163
|
+
_require_sudo()
|
164
|
+
|
165
|
+
dd_command = ["sudo", "dd", f"if={image_path}", f"of={device_path}", "bs=16M", "status=progress"]
|
166
|
+
try:
|
167
|
+
# Start dd process with unbuffered output
|
168
|
+
process = subprocess.Popen(
|
169
|
+
dd_command,
|
170
|
+
stdout=subprocess.PIPE,
|
171
|
+
stderr=subprocess.PIPE,
|
172
|
+
text=True,
|
173
|
+
bufsize=1,
|
174
|
+
universal_newlines=True,
|
175
|
+
errors="replace"
|
176
|
+
)
|
177
|
+
|
178
|
+
# Regex to parse dd progress (more robust to handle variations)
|
179
|
+
pattern = re.compile(
|
180
|
+
r"(?:(?:dd: )?(\d+)\s+bytes(?:.*?)copied)|(?:^(\d+)\s+bytes\s+transferred)",
|
181
|
+
re.IGNORECASE
|
182
|
+
)
|
183
|
+
|
184
|
+
# Initialize tqdm progress bar
|
185
|
+
with tqdm(total=file_size, unit="B", unit_scale=True, desc="Copying", ncols=100) as pbar:
|
186
|
+
with open(debug_log, "w") as log_file:
|
187
|
+
while process.poll() is None:
|
188
|
+
rlist, _, _ = select.select([process.stdout, process.stderr], [], [], 0.1)
|
189
|
+
for stream in rlist:
|
190
|
+
line = stream.readline().strip()
|
191
|
+
if line:
|
192
|
+
log_file.write(f"{line}\n")
|
193
|
+
log_file.flush()
|
194
|
+
match = pattern.search(line)
|
195
|
+
if match:
|
196
|
+
bytes_transferred = int(match.group(1))
|
197
|
+
pbar.n = min(bytes_transferred, file_size)
|
198
|
+
pbar.refresh()
|
199
|
+
elif line:
|
200
|
+
click.echo(f"⚠️ dd: {line}") # Show other messages (e.g., errors)
|
201
|
+
|
202
|
+
# Capture remaining output and check for errors
|
203
|
+
stdout, stderr = process.communicate()
|
204
|
+
with open(debug_log, "a") as log_file:
|
205
|
+
if stdout.strip():
|
206
|
+
log_file.write(f"Final stdout: {stdout.strip()}\n")
|
207
|
+
if stderr.strip():
|
208
|
+
log_file.write(f"Final stderr: {stderr.strip()}\n")
|
209
|
+
|
210
|
+
if process.returncode != 0:
|
211
|
+
click.echo(f"❌ Failed to copy {image_path} to {device_path}: {stderr}")
|
212
|
+
click.echo(f"ℹ️ Check {debug_log} for raw dd output.")
|
213
|
+
sys.exit(1)
|
214
|
+
|
215
|
+
click.echo(f"✅ Successfully copied {image_path} to {device_path}")
|
216
|
+
subprocess.run(["sync"], check=True)
|
217
|
+
click.echo("✅ Synced data to device")
|
218
|
+
except subprocess.CalledProcessError as e:
|
219
|
+
click.echo(f"❌ Failed to copy {image_path} to {device_path}: {e.stderr}")
|
220
|
+
click.echo(f"ℹ️ Check {debug_log} for raw dd output.")
|
221
|
+
sys.exit(1)
|
222
|
+
except FileNotFoundError:
|
223
|
+
click.echo("❌ 'dd' not found. Ensure both are installed and accessible.")
|
224
|
+
sys.exit(1)
|
225
|
+
|
226
|
+
def write_bootimg(image_path):
|
227
|
+
"""Write a boot image to a removable device."""
|
228
|
+
# Step 1: Validate image file
|
229
|
+
if not os.path.isfile(image_path):
|
230
|
+
click.echo(f"❌ Image file {image_path} does not exist.")
|
231
|
+
sys.exit(1)
|
232
|
+
|
233
|
+
click.echo(f"✅ Valid image file: {image_path}")
|
234
|
+
|
235
|
+
# Step 2: Check if dd is installed
|
236
|
+
if not check_dd_installed():
|
237
|
+
click.echo("⚠️ 'dd' is not installed on this system.")
|
238
|
+
if platform.system() == "Darwin":
|
239
|
+
click.echo("ℹ️ On macOS, 'dd' is included by default. Check your PATH or system configuration.")
|
240
|
+
elif platform.system() == "Linux":
|
241
|
+
click.echo("ℹ️ On Linux, install 'dd' using your package manager (e.g., 'sudo apt install coreutils' on Debian/Ubuntu).")
|
242
|
+
else:
|
243
|
+
click.echo("ℹ️ Please install 'dd' for your system.")
|
244
|
+
sys.exit(1)
|
245
|
+
click.echo("✅ 'dd' is installed")
|
246
|
+
|
247
|
+
# Step 3: Detect removable devices
|
248
|
+
devices = list_removable_devices() # Assumes this function exists
|
249
|
+
if not devices:
|
250
|
+
click.echo("⚠️ No removable devices detected. Please plug in your USB drive or SD card.")
|
251
|
+
sys.exit(1)
|
252
|
+
|
253
|
+
# Step 4: Display devices
|
254
|
+
click.echo("\n🔍 Detected removable devices:")
|
255
|
+
for i, dev in enumerate(devices):
|
256
|
+
click.echo(f" [{i}] Name: {dev['name']}, Size: {dev['size']} GB, Path: {dev['path']}")
|
257
|
+
|
258
|
+
# Step 5: Select device
|
259
|
+
selected_device = None
|
260
|
+
if len(devices) == 1:
|
261
|
+
confirm = input(f"\n✅ Do you want to use device {devices[0]['path']}? (y/N): ").strip().lower()
|
262
|
+
if confirm == 'y':
|
263
|
+
selected_device = devices[0]
|
264
|
+
else:
|
265
|
+
click.echo("❌ Operation cancelled by user.")
|
266
|
+
sys.exit(0)
|
267
|
+
else:
|
268
|
+
try:
|
269
|
+
selection = int(input("\n🔢 Multiple devices found. Enter the number of the device to use: ").strip())
|
270
|
+
if 0 <= selection < len(devices):
|
271
|
+
selected_device = devices[selection]
|
272
|
+
else:
|
273
|
+
click.echo("❌ Invalid selection.")
|
274
|
+
sys.exit(1)
|
275
|
+
except ValueError:
|
276
|
+
click.echo("❌ Invalid input. Please enter a number.")
|
277
|
+
sys.exit(1)
|
278
|
+
|
279
|
+
click.echo(f"\n🚀 Proceeding with device: {selected_device['path']}")
|
280
|
+
|
281
|
+
# Step 6: Unmount the selected device
|
282
|
+
unmount_device(selected_device['path'])
|
283
|
+
|
284
|
+
# Step 7: Copy the image to the device
|
285
|
+
copy_image_to_device(image_path, selected_device['path'])
|
286
|
+
|
287
|
+
# Step 8: Eject the device (platform-dependent)
|
288
|
+
try:
|
289
|
+
if platform.system() == "Darwin":
|
290
|
+
subprocess.run(["diskutil", "eject", selected_device['path']], check=True, capture_output=True, text=True)
|
291
|
+
click.echo(f"✅ Ejected {selected_device['path']} on macOS")
|
292
|
+
elif platform.system() == "Linux":
|
293
|
+
click.echo("ℹ️ Linux does not require explicit eject. Device is ready.")
|
294
|
+
else:
|
295
|
+
click.echo("ℹ️ Please manually eject the device if required.")
|
296
|
+
except subprocess.CalledProcessError as e:
|
297
|
+
click.echo(f"⚠️ Failed to eject {selected_device['path']}: {e.stderr}")
|
298
|
+
click.echo("ℹ️ Please manually eject the device.")
|
299
|
+
|
300
|
+
|
301
|
+
def write_image(version: str, board: str, swtype: str, internal: bool = False):
|
302
|
+
"""
|
303
|
+
Download and write a bootable firmware image to a removable storage device.
|
304
|
+
|
305
|
+
Parameters:
|
306
|
+
version (str): Firmware version to download (e.g., "1.6.0").
|
307
|
+
board (str): Target board type, e.g., "modalix" or "mlsoc".
|
308
|
+
swtype (str): Software image type, e.g., "yocto" or "exlr".
|
309
|
+
internal (bool): Whether to use internal download sources. Defaults to False.
|
310
|
+
|
311
|
+
Raises:
|
312
|
+
RuntimeError: If the download or write process fails.
|
313
|
+
"""
|
314
|
+
try:
|
315
|
+
click.echo(f"⬇️ Downloading boot image for version: {version}, board: {board}, swtype: {swtype}")
|
316
|
+
file_list = download_image(version, board, swtype, internal, update_type='bootimg')
|
317
|
+
if not isinstance(file_list, list):
|
318
|
+
raise ValueError("Expected list of extracted files, got something else.")
|
319
|
+
|
320
|
+
wic_file = next((f for f in file_list if f.endswith(".wic")), None)
|
321
|
+
if not wic_file:
|
322
|
+
raise FileNotFoundError("No .wic image file found after extraction.")
|
323
|
+
|
324
|
+
except Exception as e:
|
325
|
+
raise RuntimeError(f"❌ Failed to download image: {e}")
|
326
|
+
|
327
|
+
try:
|
328
|
+
click.echo(f"📝 Writing image to removable media: {wic_file}")
|
329
|
+
write_bootimg(wic_file)
|
330
|
+
except Exception as e:
|
331
|
+
raise RuntimeError(f"❌ Failed to write image: {e}")
|
332
|
+
|
333
|
+
|
334
|
+
if __name__ == "__main__":
|
335
|
+
if len(sys.argv) != 2:
|
336
|
+
click.echo("❌ Usage: python write_bootimg.py <image_file>")
|
337
|
+
sys.exit(1)
|
338
|
+
|
339
|
+
write_image('1.7.0', 'modalix', 'davinci', True)
|
sima_cli/update/local.py
CHANGED
@@ -46,26 +46,31 @@ def _run_local_cmd(command: str, passwd: str) -> bool:
|
|
46
46
|
|
47
47
|
def get_local_board_info() -> Tuple[str, str]:
|
48
48
|
"""
|
49
|
-
Retrieve the local board type and build version by reading /etc/build.
|
49
|
+
Retrieve the local board type and build version by reading /etc/build or /etc/buildinfo.
|
50
50
|
|
51
51
|
Returns:
|
52
52
|
(board_type, build_version): Tuple of strings, or ('', '') on failure.
|
53
53
|
"""
|
54
54
|
board_type = ""
|
55
55
|
build_version = ""
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
56
|
+
build_file_paths = ["/etc/build", "/etc/buildinfo"]
|
57
|
+
|
58
|
+
for path in build_file_paths:
|
59
|
+
try:
|
60
|
+
if os.path.isfile(path):
|
61
|
+
with open(path, "r") as f:
|
62
|
+
for line in f:
|
63
|
+
line = line.strip()
|
64
|
+
if line.startswith("MACHINE"):
|
65
|
+
board_type = line.split("=", 1)[-1].strip()
|
66
|
+
elif line.startswith("SIMA_BUILD_VERSION"):
|
67
|
+
build_version = line.split("=", 1)[-1].strip()
|
68
|
+
if board_type or build_version:
|
69
|
+
break # Exit early if data found
|
70
|
+
except Exception:
|
71
|
+
continue
|
72
|
+
|
73
|
+
return board_type, build_version
|
69
74
|
|
70
75
|
|
71
76
|
def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str):
|
sima_cli/update/query.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import re
|
1
2
|
import requests
|
2
3
|
from sima_cli.utils.config_loader import load_resource_config, artifactory_url
|
3
4
|
from sima_cli.utils.config import get_auth_token
|
@@ -45,6 +46,34 @@ def _list_available_firmware_versions_internal(board: str, match_keyword: str =
|
|
45
46
|
|
46
47
|
return top_level_folders
|
47
48
|
|
49
|
+
def _list_available_firmware_versions_external(board: str, match_keyword: str = None):
|
50
|
+
"""
|
51
|
+
Construct and return a list containing a single firmware download URL for a given board.
|
52
|
+
|
53
|
+
If match_keyword is provided and matches a 'major.minor' version pattern (e.g., '1.6'),
|
54
|
+
it is normalized to 'major.minor.patch' format (e.g., '1.6.0') to ensure consistent URL construction.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
board (str): The name of the hardware board.
|
58
|
+
match_keyword (str, optional): A version string to match (e.g., '1.6' or '1.6.0').
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
list[str]: A list containing one formatted firmware download URL.
|
62
|
+
"""
|
63
|
+
fwtype = 'yocto'
|
64
|
+
cfg = load_resource_config()
|
65
|
+
download_url_base = cfg.get('public').get('download').get('download_url')
|
66
|
+
|
67
|
+
if match_keyword:
|
68
|
+
if re.fullmatch(r'\d+\.\d+', match_keyword):
|
69
|
+
match_keyword += '.0'
|
70
|
+
|
71
|
+
firmware_download_url = (
|
72
|
+
f'{download_url_base}SDK{match_keyword}/devkit/{board}/{fwtype}/'
|
73
|
+
f'simaai-devkit-fw-{board}-{fwtype}-{match_keyword}.tar.gz'
|
74
|
+
)
|
75
|
+
return [firmware_download_url]
|
76
|
+
|
48
77
|
|
49
78
|
def list_available_firmware_versions(board: str, match_keyword: str = None, internal: bool = False):
|
50
79
|
"""
|
@@ -59,6 +88,6 @@ def list_available_firmware_versions(board: str, match_keyword: str = None, inte
|
|
59
88
|
- List[str] of firmware version folder names, or None if access is not allowed
|
60
89
|
"""
|
61
90
|
if not internal:
|
62
|
-
|
91
|
+
return _list_available_firmware_versions_external(board, match_keyword)
|
63
92
|
|
64
93
|
return _list_available_firmware_versions_internal(board, match_keyword)
|
sima_cli/update/remote.py
CHANGED
@@ -57,7 +57,6 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
57
57
|
|
58
58
|
Args:
|
59
59
|
ip (str): IP address of the board.
|
60
|
-
timeout (int): SSH timeout in seconds.
|
61
60
|
|
62
61
|
Returns:
|
63
62
|
(board_type, build_version): Tuple of strings, or ('', '') on failure.
|
@@ -70,20 +69,22 @@ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> Tuple[str,
|
|
70
69
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
71
70
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
72
71
|
|
73
|
-
|
72
|
+
# Try /etc/build first then /etc/buildinfo
|
73
|
+
stdin, stdout, stderr = ssh.exec_command("cat /etc/build 2>/dev/null || cat /etc/buildinfo 2>/dev/null")
|
74
74
|
output = stdout.read().decode()
|
75
75
|
ssh.close()
|
76
76
|
|
77
77
|
for line in output.splitlines():
|
78
78
|
line = line.strip()
|
79
79
|
if line.startswith("MACHINE"):
|
80
|
-
board_type = line.split("=")[-1].strip()
|
80
|
+
board_type = line.split("=", 1)[-1].strip()
|
81
81
|
elif line.startswith("SIMA_BUILD_VERSION"):
|
82
|
-
build_version = line.split("=")[-1].strip()
|
82
|
+
build_version = line.split("=", 1)[-1].strip()
|
83
83
|
|
84
84
|
return board_type, build_version
|
85
85
|
|
86
|
-
except Exception:
|
86
|
+
except Exception as e:
|
87
|
+
click.echo(f"Unable to retrieve board info {e}")
|
87
88
|
return "", ""
|
88
89
|
|
89
90
|
|
@@ -124,7 +125,7 @@ def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
|
|
124
125
|
if stdout.channel.recv_ready():
|
125
126
|
output = stdout.channel.recv(4096).decode("utf-8", errors="replace")
|
126
127
|
for line in output.splitlines():
|
127
|
-
click.echo(f"
|
128
|
+
click.echo(f"↦ {line}")
|
128
129
|
if stdout.channel.recv_stderr_ready():
|
129
130
|
err_output = stdout.channel.recv_stderr(4096).decode("utf-8", errors="replace")
|
130
131
|
for line in err_output.splitlines():
|
@@ -133,7 +134,7 @@ def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
|
|
133
134
|
# Final remaining output
|
134
135
|
remaining = stdout.read().decode("utf-8", errors="replace")
|
135
136
|
for line in remaining.splitlines():
|
136
|
-
click.echo(f"
|
137
|
+
click.echo(f"↦ {line}")
|
137
138
|
|
138
139
|
remaining_err = stderr.read().decode("utf-8", errors="replace")
|
139
140
|
for line in remaining_err.splitlines():
|
@@ -180,9 +181,13 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
180
181
|
ssh,
|
181
182
|
f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
|
182
183
|
)
|
183
|
-
click.echo("✅ tRoot update complete.")
|
184
|
+
click.echo("✅ tRoot update complete, the board needs to be rebooted to proceed to the next phase of update.")
|
185
|
+
click.confirm("⚠️ Have you rebooted the board?", default=True, abort=True)
|
186
|
+
_wait_for_ssh(ip, timeout=120)
|
184
187
|
|
185
188
|
# Upload Palette image
|
189
|
+
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
190
|
+
sftp = ssh.open_sftp()
|
186
191
|
_scp_file(sftp, palette_path, os.path.join(remote_dir, palette_name))
|
187
192
|
click.echo("🚀 Uploaded system image.")
|
188
193
|
|
@@ -205,7 +210,7 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
205
210
|
_run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
|
206
211
|
|
207
212
|
except Exception as reboot_err:
|
208
|
-
click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the
|
213
|
+
click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the board...")
|
209
214
|
|
210
215
|
try:
|
211
216
|
ssh.close()
|
@@ -219,11 +224,12 @@ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, pa
|
|
219
224
|
# Reconnect and verify version
|
220
225
|
try:
|
221
226
|
click.echo("🔍 Reconnecting to verify build version...")
|
227
|
+
time.sleep(10)
|
222
228
|
ssh = paramiko.SSHClient()
|
223
229
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
224
230
|
ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
|
225
231
|
|
226
|
-
_run_remote_command(ssh, "
|
232
|
+
_run_remote_command(ssh, "grep SIMA_BUILD_VERSION /etc/build 2>/dev/null || grep SIMA_BUILD_VERSION /etc/buildinfo 2>/dev/null", password=passwd)
|
227
233
|
ssh.close()
|
228
234
|
except Exception as e:
|
229
235
|
click.echo(f"❌ Unable to validate the version: {e}")
|