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.
@@ -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
- build_file_path = "/etc/build"
57
-
58
- try:
59
- with open(build_file_path, "r") as f:
60
- for line in f:
61
- line = line.strip()
62
- if line.startswith("MACHINE"):
63
- board_type = line.split("=")[-1].strip()
64
- elif line.startswith("SIMA_BUILD_VERSION"):
65
- build_version = line.split("=")[-1].strip()
66
- return board_type, build_version
67
- except Exception:
68
- return "", ""
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
- raise PermissionError("Internal access required to list firmware versions.")
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
- stdin, stdout, stderr = ssh.exec_command("cat /etc/build")
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"📄 {line}")
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"📄 {line}")
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 device...")
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, "cat /etc/build | grep SIMA_BUILD_VERSION", password=passwd)
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}")