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/__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
@@ -9,10 +9,13 @@ from sima_cli.__version__ import __version__
|
|
9
9
|
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
|
+
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
|
12
15
|
|
13
16
|
# Entry point for the CLI tool using Click's command group decorator
|
14
17
|
@click.group()
|
15
|
-
@click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources
|
18
|
+
@click.option('-i', '--internal', is_flag=True, help="Use internal Artifactory resources, Authorized Sima employees only")
|
16
19
|
@click.pass_context
|
17
20
|
def main(ctx, internal):
|
18
21
|
"""
|
@@ -139,12 +142,13 @@ def download(ctx, url, dest):
|
|
139
142
|
help="Skip confirmation after firmware file is downloaded."
|
140
143
|
)
|
141
144
|
@click.option(
|
142
|
-
'--passwd',
|
145
|
+
'-p', '--passwd',
|
143
146
|
default='edgeai',
|
144
147
|
help="Optional SSH password for remote board (default is 'edgeai')."
|
145
148
|
)
|
149
|
+
@click.option("-f", "--flavor", type=click.Choice(["headless", "full"], case_sensitive=False), default="headless", show_default=True, help="firmware flavor.")
|
146
150
|
@click.pass_context
|
147
|
-
def update(ctx, version_or_url, ip, yes, passwd):
|
151
|
+
def update(ctx, version_or_url, ip, yes, passwd, flavor):
|
148
152
|
"""
|
149
153
|
Run system update across different environments.
|
150
154
|
Downloads and applies firmware updates for PCIe host or SiMa board.
|
@@ -152,7 +156,7 @@ def update(ctx, version_or_url, ip, yes, passwd):
|
|
152
156
|
version_or_url: The version string (e.g. '1.5.0') or a direct URL to the firmware package.
|
153
157
|
"""
|
154
158
|
internal = ctx.obj.get("internal", False)
|
155
|
-
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)
|
156
160
|
|
157
161
|
# ----------------------
|
158
162
|
# Model Zoo Subcommands
|
@@ -225,29 +229,42 @@ def show_mla_memory_usage(ctx):
|
|
225
229
|
# ----------------------
|
226
230
|
@main.command(name="bootimg")
|
227
231
|
@click.option("-v", "--version", required=True, help="Firmware version to download and write (e.g., 1.6.0)")
|
228
|
-
@click.option("--boardtype", type=click.Choice(["modalix",
|
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")
|
229
235
|
@click.pass_context
|
230
|
-
def bootimg_cmd(ctx, version, boardtype):
|
236
|
+
def bootimg_cmd(ctx, version, boardtype, netboot, autoflash):
|
231
237
|
"""
|
232
|
-
Download and burn a
|
238
|
+
Download and burn a removable media or setup TFTP boot.
|
233
239
|
|
240
|
+
Only supports headless image
|
241
|
+
|
234
242
|
Examples:
|
235
243
|
sima-cli bootimg -v 1.6.0
|
236
244
|
sima-cli bootimg -v 1.6.0 --boardtype mlsoc
|
237
|
-
sima-cli bootimg -v 1.6.0 --boardtype
|
245
|
+
sima-cli bootimg -v 1.6.0 --boardtype mlsoc
|
246
|
+
sima-cli bootimg -v 1.6.0 --boardtype modalix --netboot
|
247
|
+
sima-cli bootimg -v 1.6.0 --boardtype modalix --autoflash
|
238
248
|
"""
|
239
249
|
|
240
250
|
from sima_cli.update.bootimg import write_image
|
251
|
+
from sima_cli.update.netboot import setup_netboot
|
241
252
|
|
242
253
|
internal = ctx.obj.get("internal", False)
|
243
254
|
|
244
255
|
click.echo(f"📦 Preparing boot image:")
|
245
256
|
click.echo(f" 🔹 Version : {version}")
|
246
257
|
click.echo(f" 🔹 Board Type: {boardtype}")
|
258
|
+
click.echo(f" 🔹 F/W Flavor: headless")
|
247
259
|
|
248
260
|
try:
|
249
261
|
boardtype = boardtype if boardtype != 'mlsoc' else 'davinci'
|
250
|
-
|
262
|
+
if netboot or autoflash:
|
263
|
+
setup_netboot(version, boardtype, internal, autoflash, flavor='headless')
|
264
|
+
click.echo("✅ Netboot image prepared and TFTP server is running.")
|
265
|
+
else:
|
266
|
+
write_image(version, boardtype, 'yocto', internal, flavor='headless')
|
267
|
+
click.echo("✅ Boot image successfully written.")
|
251
268
|
click.echo("✅ Boot image successfully written.")
|
252
269
|
except Exception as e:
|
253
270
|
click.echo(f"❌ Failed to write boot image: {e}", err=True)
|
@@ -269,10 +286,10 @@ def install_cmd(ctx, component, version):
|
|
269
286
|
Install supported components such as SDKs or tools.
|
270
287
|
|
271
288
|
Examples:
|
272
|
-
cli install palette -v 1.6.0
|
273
|
-
cli install hostdriver -v 1.6.0
|
274
289
|
|
275
|
-
|
290
|
+
sima-cli install hostdriver -v 1.6.0
|
291
|
+
|
292
|
+
sima-cli install optiview
|
276
293
|
"""
|
277
294
|
component = component.lower()
|
278
295
|
internal = ctx.obj.get("internal", False)
|
@@ -295,7 +312,78 @@ def install_cmd(ctx, component, version):
|
|
295
312
|
install_optiview()
|
296
313
|
|
297
314
|
click.echo("✅ Installation complete.")
|
315
|
+
|
316
|
+
# ----------------------
|
317
|
+
# Serial Subcommands
|
318
|
+
# ----------------------
|
319
|
+
@main.command(name="serial")
|
320
|
+
@click.option("-b", "--baud", default=115200, show_default=True, help="Baud rate for the serial connection")
|
321
|
+
@click.pass_context
|
322
|
+
def serial_cmd(ctx, baud):
|
323
|
+
"""
|
324
|
+
Connect to the UART serial console of the DevKit.
|
325
|
+
|
326
|
+
Automatically detects the serial port and launches a terminal emulator:
|
327
|
+
|
328
|
+
- macOS: uses 'picocom'
|
329
|
+
|
330
|
+
- Linux: uses 'picocom'
|
298
331
|
|
332
|
+
- Windows: shows PuTTY/Tera Term setup instructions
|
333
|
+
"""
|
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)
|
299
387
|
|
300
388
|
# ----------------------
|
301
389
|
# App Zoo Subcommands
|
sima_cli/install/optiview.py
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
import sys
|
2
2
|
import platform
|
3
3
|
import click
|
4
|
-
import urllib
|
5
4
|
import json
|
6
|
-
import tempfile
|
7
5
|
import tarfile
|
8
6
|
import os
|
9
7
|
import subprocess
|
@@ -31,6 +29,12 @@ def install_optiview():
|
|
31
29
|
version_url = f"{download_url_base}optiview/metadata.json"
|
32
30
|
downloads_dir = Path.home() / "Downloads" / "optiview-installer"
|
33
31
|
downloads_dir.mkdir(parents=True, exist_ok=True)
|
32
|
+
|
33
|
+
# Always redownload the metadata file to get the latest version
|
34
|
+
metadata_path = downloads_dir / "metadata.json"
|
35
|
+
if metadata_path.exists():
|
36
|
+
metadata_path.unlink()
|
37
|
+
|
34
38
|
metadata_path = download_file_from_url(version_url, dest_folder=downloads_dir, internal=False)
|
35
39
|
|
36
40
|
with open(metadata_path, "r") as f:
|
@@ -70,7 +74,8 @@ def install_optiview():
|
|
70
74
|
else:
|
71
75
|
subprocess.run(["bash", os.path.basename(script_path)], check=True, cwd=downloads_dir)
|
72
76
|
|
73
|
-
|
77
|
+
script_name = "run.bat" if platform.system() == "Windows" else "run.sh"
|
78
|
+
click.echo(f"✅ Optiview installed successfully. Run {downloads_dir}/{script_name} to start OptiView")
|
74
79
|
|
75
80
|
except Exception as e:
|
76
81
|
click.echo(f"❌ Installation failed: {e}")
|
@@ -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}")
|
File without changes
|
@@ -0,0 +1,114 @@
|
|
1
|
+
import platform
|
2
|
+
import subprocess
|
3
|
+
import shutil
|
4
|
+
import click
|
5
|
+
from sima_cli.utils.env import is_sima_board
|
6
|
+
|
7
|
+
def connect_serial(ctx, baud):
|
8
|
+
"""
|
9
|
+
Connect to the UART serial console of the DevKit.
|
10
|
+
Automatically installs required tools if missing.
|
11
|
+
"""
|
12
|
+
if is_sima_board():
|
13
|
+
click.echo("🚫 This command is not supported on the DevKit. Please run it from your host machine.")
|
14
|
+
ctx.exit(1)
|
15
|
+
|
16
|
+
system = platform.system()
|
17
|
+
internal = ctx.obj.get("internal", False)
|
18
|
+
|
19
|
+
if system == "Darwin":
|
20
|
+
_connect_mac(baud)
|
21
|
+
elif system == "Linux":
|
22
|
+
_connect_linux(baud)
|
23
|
+
elif system == "Windows":
|
24
|
+
_print_windows_instructions()
|
25
|
+
else:
|
26
|
+
click.echo(f"⚠️ Unsupported OS: {system}. Only macOS, Linux, and Windows are supported.")
|
27
|
+
ctx.exit(1)
|
28
|
+
|
29
|
+
click.echo("✅ Serial session ended.")
|
30
|
+
|
31
|
+
|
32
|
+
def _connect_mac(baud):
|
33
|
+
terminal = "picocom"
|
34
|
+
if not shutil.which(terminal):
|
35
|
+
click.echo("⚙️ 'picocom' is not installed. Attempting to install with Homebrew...")
|
36
|
+
if shutil.which("brew"):
|
37
|
+
subprocess.run(["brew", "install", "picocom"], check=True)
|
38
|
+
else:
|
39
|
+
click.echo("❌ Homebrew not found. Please install Homebrew first: https://brew.sh/")
|
40
|
+
raise SystemExit(1)
|
41
|
+
|
42
|
+
ports = sorted(
|
43
|
+
subprocess.getoutput("ls /dev/tty.usbserial-* /dev/cu.usbserial-* 2>/dev/null").splitlines()
|
44
|
+
)
|
45
|
+
if not ports:
|
46
|
+
click.echo("❌ No USB serial device found.")
|
47
|
+
raise SystemExit(1)
|
48
|
+
|
49
|
+
click.echo(f"Connecting to device with picocom ({baud} baud)...")
|
50
|
+
click.echo("🧷 To exit: Press Ctrl + A, then Ctrl + X")
|
51
|
+
click.echo("📜 Scrollback will work in your terminal as expected.\n")
|
52
|
+
|
53
|
+
if not click.confirm("Proceed to connect?", default=True):
|
54
|
+
click.echo("❎ Connection aborted by user.")
|
55
|
+
return
|
56
|
+
|
57
|
+
port = ports[0]
|
58
|
+
click.echo(f"🔌 Connecting to {port} with picocom (115200 8N1)...")
|
59
|
+
try:
|
60
|
+
subprocess.run([
|
61
|
+
terminal,
|
62
|
+
"-b", f"{baud}",
|
63
|
+
"--databits", "8",
|
64
|
+
"--parity", "n",
|
65
|
+
"--stopbits", "1",
|
66
|
+
port
|
67
|
+
])
|
68
|
+
except KeyboardInterrupt:
|
69
|
+
click.echo("\n❎ Serial connection interrupted by user.")
|
70
|
+
|
71
|
+
|
72
|
+
def _connect_linux(baud):
|
73
|
+
terminal = "picocom"
|
74
|
+
if not shutil.which(terminal):
|
75
|
+
click.echo("⚙️ 'picocom' is not installed. Attempting to install via apt...")
|
76
|
+
if shutil.which("apt-get"):
|
77
|
+
subprocess.run(["sudo", "apt-get", "update"], check=True)
|
78
|
+
subprocess.run(["sudo", "apt-get", "install", "-y", "picocom"], check=True)
|
79
|
+
else:
|
80
|
+
click.echo("❌ 'apt-get' not found. Please install picocom manually.")
|
81
|
+
raise SystemExit(1)
|
82
|
+
|
83
|
+
ports = sorted(
|
84
|
+
subprocess.getoutput("ls /dev/ttyUSB* 2>/dev/null").splitlines()
|
85
|
+
)
|
86
|
+
if not ports:
|
87
|
+
click.echo("❌ No USB serial device found.")
|
88
|
+
raise SystemExit(1)
|
89
|
+
|
90
|
+
port = ports[0]
|
91
|
+
click.echo(f"🔌 Connecting to {port} with picocom ({baud} 8N1)...")
|
92
|
+
try:
|
93
|
+
subprocess.run(
|
94
|
+
["sudo", terminal, "-b", f"{baud}", "--databits", "8", "--parity", "n", "--stopbits", "1", port]
|
95
|
+
)
|
96
|
+
except KeyboardInterrupt:
|
97
|
+
click.echo("\n❎ Serial connection interrupted by user.")
|
98
|
+
|
99
|
+
|
100
|
+
def _print_windows_instructions():
|
101
|
+
click.echo("📘 To connect to the DevKit via a serial terminal on Windows, follow these steps:\n")
|
102
|
+
|
103
|
+
click.echo("1. Identify the COM Port:")
|
104
|
+
click.echo(" • Open **Device Manager** → Expand **Ports (COM & LPT)**.")
|
105
|
+
click.echo(" • Look for an entry like **USB Serial Port (COMx)**.\n")
|
106
|
+
|
107
|
+
click.echo("2. Install and Open a Serial Terminal:")
|
108
|
+
click.echo(" • Use **PuTTY** (Download from https://www.putty.org/) or **Tera Term**.")
|
109
|
+
click.echo(" • Set the **Connection Type** to **Serial**.")
|
110
|
+
click.echo(" • Enter the correct **COM Port** (e.g., COM3).")
|
111
|
+
click.echo(" • Set **Baud Rate** to **115200**.")
|
112
|
+
click.echo(" • Click **Open** to start the connection.\n")
|
113
|
+
|
114
|
+
click.echo("🔌 You are now ready to connect to the DevKit over serial.")
|