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 CHANGED
@@ -1,2 +1,2 @@
1
1
  # sima_cli/__version__.py
2
- __version__ = "0.0.19"
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", "mlsoc"], case_sensitive=False), default="mlsoc", show_default=True, help="Target board type.")
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 bootable image onto removable media (e.g., SD card or USB stick).
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 modalix
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
- write_image(version, boardtype, 'yocto', internal)
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
- cli install optiview
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
@@ -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
- click.echo(f" Optiview installed successfully. Run {downloads_dir}/run.sh to start optiview")
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.")