sima-cli 0.0.1__py3-none-any.whl → 0.0.12__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,94 @@
1
+ import os
2
+ import subprocess
3
+ import tempfile
4
+ from typing import List
5
+ import pty
6
+ import click
7
+
8
+ def _run_local_cmd(command: str, passwd: str) -> bool:
9
+ """
10
+ Run a local command using a pseudo-terminal (pty) to force live output flushing,
11
+ and optionally pass a sudo password.
12
+ """
13
+ click.echo(f"🖥️ Running: {command}")
14
+
15
+ needs_sudo = command.strip().startswith("sudo")
16
+ if needs_sudo:
17
+ command = f"sudo -S {command[len('sudo '):]}"
18
+
19
+ try:
20
+ pid, fd = pty.fork()
21
+
22
+ if pid == 0:
23
+ # Child process: execute the shell command
24
+ os.execvp("sh", ["sh", "-c", command])
25
+ else:
26
+ if needs_sudo:
27
+ os.write(fd, (passwd + "\n").encode())
28
+
29
+ while True:
30
+ try:
31
+ output = os.read(fd, 1024).decode()
32
+ if not output:
33
+ break
34
+ for line in output.splitlines():
35
+ click.echo(line)
36
+ except OSError:
37
+ break
38
+
39
+ _, status = os.waitpid(pid, 0)
40
+ return os.WIFEXITED(status) and os.WEXITSTATUS(status) == 0
41
+
42
+ except Exception as e:
43
+ click.echo(f"❌ Command execution error: {e}")
44
+ return False
45
+
46
+
47
+ def get_local_board_info() -> tuple[str, str]:
48
+ """
49
+ Retrieve the local board type and build version by reading /etc/build.
50
+
51
+ Returns:
52
+ (board_type, build_version): Tuple of strings, or ('', '') on failure.
53
+ """
54
+ board_type = ""
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 "", ""
69
+
70
+
71
+ def push_and_update_local_board(troot_path: str, palette_path: str, passwd: str):
72
+ """
73
+ Perform local firmware update using swupdate commands.
74
+ Calls swupdate directly on the provided file paths.
75
+ """
76
+ click.echo("📦 Starting local firmware update...")
77
+
78
+ try:
79
+ # Run tRoot update
80
+ click.echo("⚙️ Flashing tRoot image...")
81
+ if not _run_local_cmd(f"sudo swupdate -H simaai-image-troot:1.0 -i {troot_path}", passwd):
82
+ click.echo("❌ tRoot update failed.")
83
+ return
84
+ click.echo("✅ tRoot update completed.")
85
+
86
+ # Run Palette update
87
+ click.echo("⚙️ Flashing System image...")
88
+ if not _run_local_cmd(f"sudo swupdate -H simaai-image-palette:1.0 -i {palette_path}", passwd):
89
+ click.echo("❌ System image update failed.")
90
+ return
91
+ click.echo("✅ System image update completed. Please powercycle the device")
92
+
93
+ except Exception as e:
94
+ click.echo(f"❌ Local update failed: {e}")
@@ -0,0 +1,238 @@
1
+ import paramiko
2
+ import os
3
+ import click
4
+ import time
5
+ import socket
6
+ import itertools
7
+ import threading
8
+ import sys
9
+ import select
10
+
11
+ DEFAULT_USER = "sima"
12
+ DEFAULT_PASSWORD = "edgeai"
13
+
14
+ def _wait_for_ssh(ip: str, timeout: int = 120):
15
+ """
16
+ Show an animated spinner while waiting for SSH on the target IP to become available.
17
+
18
+ Args:
19
+ ip (str): IP address of the target board.
20
+ timeout (int): Maximum seconds to wait.
21
+ """
22
+ spinner = itertools.cycle(['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏'])
23
+ stop_event = threading.Event()
24
+
25
+ def animate():
26
+ while not stop_event.is_set():
27
+ sys.stdout.write(f"\r🔁 Waiting for board to reboot {next(spinner)} ")
28
+ sys.stdout.flush()
29
+ time.sleep(0.1)
30
+
31
+ thread = threading.Thread(target=animate)
32
+ thread.start()
33
+
34
+ start_time = time.time()
35
+ success = False
36
+ while time.time() - start_time < timeout:
37
+ try:
38
+ sock = socket.create_connection((ip, 22), timeout=3)
39
+ sock.close()
40
+ success = True
41
+ break
42
+ except (socket.error, paramiko.ssh_exception.SSHException):
43
+ time.sleep(2) # wait and retry
44
+
45
+ stop_event.set()
46
+ thread.join()
47
+
48
+ if not success:
49
+ print(f"❌ Timeout: SSH did not become available on {ip} within {timeout} seconds.")
50
+ else:
51
+ print("\r✅ Board is online! \n")
52
+
53
+ def get_remote_board_info(ip: str, passwd: str = DEFAULT_PASSWORD) -> tuple[str, str]:
54
+ """
55
+ Connect to the remote board and retrieve board type and build version.
56
+
57
+ Args:
58
+ ip (str): IP address of the board.
59
+ timeout (int): SSH timeout in seconds.
60
+
61
+ Returns:
62
+ (board_type, build_version): Tuple of strings, or ('', '') on failure.
63
+ """
64
+ board_type = ""
65
+ build_version = ""
66
+
67
+ try:
68
+ ssh = paramiko.SSHClient()
69
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
70
+ ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
71
+
72
+ stdin, stdout, stderr = ssh.exec_command("cat /etc/build")
73
+ output = stdout.read().decode()
74
+ ssh.close()
75
+
76
+ for line in output.splitlines():
77
+ line = line.strip()
78
+ if line.startswith("MACHINE"):
79
+ board_type = line.split("=")[-1].strip()
80
+ elif line.startswith("SIMA_BUILD_VERSION"):
81
+ build_version = line.split("=")[-1].strip()
82
+
83
+ return board_type, build_version
84
+
85
+ except Exception:
86
+ return "", ""
87
+
88
+
89
+ def _scp_file(sftp, local_path: str, remote_path: str):
90
+ """Upload file via SFTP and report."""
91
+ filename = os.path.basename(local_path)
92
+ click.echo(f"📤 Uploading {filename} → {remote_path}")
93
+ sftp.put(local_path, remote_path)
94
+ click.echo("✅ Upload complete")
95
+
96
+ def _run_remote_command(ssh, command: str, password: str = DEFAULT_PASSWORD):
97
+ """
98
+ Run a remote command over SSH and stream its output live to the console.
99
+ If the command starts with 'sudo', pipe in the password.
100
+
101
+ Args:
102
+ ssh (paramiko.SSHClient): Active SSH connection.
103
+ command (str): The command to run on the remote host.
104
+ password (str): Password to use if the command requires sudo.
105
+ """
106
+ click.echo(f"🚀 Running on remote: {command}")
107
+
108
+ needs_sudo = command.strip().startswith("sudo")
109
+ if needs_sudo:
110
+ # Use -S to allow password from stdin
111
+ command = f"sudo -S {command[len('sudo '):]}"
112
+
113
+ stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
114
+
115
+ if needs_sudo:
116
+ # Send password immediately, followed by newline
117
+ stdin.write(password + "\n")
118
+ stdin.flush()
119
+
120
+ while not stdout.channel.exit_status_ready():
121
+ rl, _, _ = select.select([stdout.channel], [], [], 0.5)
122
+ if rl:
123
+ if stdout.channel.recv_ready():
124
+ output = stdout.channel.recv(4096).decode("utf-8", errors="replace")
125
+ for line in output.splitlines():
126
+ click.echo(f"📄 {line}")
127
+ if stdout.channel.recv_stderr_ready():
128
+ err_output = stdout.channel.recv_stderr(4096).decode("utf-8", errors="replace")
129
+ for line in err_output.splitlines():
130
+ click.echo(f"⚠️ {line}")
131
+
132
+ # Final remaining output
133
+ remaining = stdout.read().decode("utf-8", errors="replace")
134
+ for line in remaining.splitlines():
135
+ click.echo(f"📄 {line}")
136
+
137
+ remaining_err = stderr.read().decode("utf-8", errors="replace")
138
+ for line in remaining_err.splitlines():
139
+ click.echo(f"⚠️ {line}")
140
+
141
+ def reboot_remote_board(ip: str, passwd: str):
142
+ """
143
+ Reboot remote board by sending SSH command
144
+ """
145
+ try:
146
+ ssh = paramiko.SSHClient()
147
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
148
+
149
+ ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
150
+
151
+ _run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
152
+ _run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
153
+
154
+ except Exception as reboot_err:
155
+ click.echo(f"⚠️ Unable to connect to the remote board")
156
+
157
+ def push_and_update_remote_board(ip: str, troot_path: str, palette_path: str, passwd: str, reboot_and_wait: bool):
158
+ """
159
+ Upload and install firmware images to remote board over SSH.
160
+ Assumes default credentials: sima / edgeai.
161
+ Includes reboot and SSH wait after each step.
162
+ """
163
+ ssh = paramiko.SSHClient()
164
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
165
+
166
+ try:
167
+ ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
168
+ sftp = ssh.open_sftp()
169
+ remote_dir = "/tmp"
170
+
171
+ # Upload tRoot image
172
+ troot_name = os.path.basename(troot_path)
173
+ palette_name = os.path.basename(palette_path)
174
+ _scp_file(sftp, troot_path, os.path.join(remote_dir, troot_name))
175
+ click.echo("🚀 Uploaded tRoot image.")
176
+
177
+ # Run tRoot update
178
+ _run_remote_command(
179
+ ssh,
180
+ f"sudo swupdate -H simaai-image-troot:1.0 -i /tmp/{troot_name}", password=passwd
181
+ )
182
+ click.echo("✅ tRoot update complete.")
183
+
184
+ # Upload Palette image
185
+ _scp_file(sftp, palette_path, os.path.join(remote_dir, palette_name))
186
+ click.echo("🚀 Uploaded system image.")
187
+
188
+ # Run Palette update
189
+ _run_remote_command(
190
+ ssh,
191
+ f"sudo swupdate -H simaai-image-palette:1.0 -i /tmp/{palette_name}",
192
+ password=passwd
193
+ )
194
+ click.echo("✅ Board image update complete.")
195
+
196
+ # 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
197
+ # After that we can reboot the whole system.
198
+ if reboot_and_wait:
199
+ # Reboot and expect disconnect
200
+ click.echo("🔁 Rebooting board after update. Waiting for reconnection...")
201
+
202
+ try:
203
+ _run_remote_command(ssh, "sudo systemctl stop watchdog", password=passwd)
204
+ _run_remote_command(ssh, "sudo bash -c 'echo b > /proc/sysrq-trigger'", password=passwd)
205
+
206
+ except Exception as reboot_err:
207
+ click.echo(f"⚠️ SSH connection lost due to reboot (expected): {reboot_err}, please powercycle the device...")
208
+
209
+ try:
210
+ ssh.close()
211
+ except Exception:
212
+ pass
213
+
214
+ # Wait for board to come back
215
+ time.sleep(5)
216
+ _wait_for_ssh(ip, timeout=120)
217
+
218
+ # Reconnect and verify version
219
+ try:
220
+ click.echo("🔍 Reconnecting to verify build version...")
221
+ ssh = paramiko.SSHClient()
222
+ ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
223
+ ssh.connect(ip, username=DEFAULT_USER, password=passwd, timeout=10)
224
+
225
+ _run_remote_command(ssh, "cat /etc/build | grep SIMA_BUILD_VERSION", password=passwd)
226
+ ssh.close()
227
+ except Exception as e:
228
+ click.echo(f"❌ Unable to validate the version: {e}")
229
+
230
+ click.echo("✅ Firmware update process complete.")
231
+
232
+ except Exception as e:
233
+ click.echo(f"❌ Remote update failed: {e}")
234
+
235
+
236
+ if __name__ == "__main__":
237
+ _wait_for_ssh("192.168.2.20", timeout=60)
238
+ print(get_remote_board_info("192.168.2.20"))