sdev 0.7.10__tar.gz → 0.7.12__tar.gz
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.
- {sdev-0.7.10/sdev.egg-info → sdev-0.7.12}/PKG-INFO +1 -1
- {sdev-0.7.10 → sdev-0.7.12}/pyproject.toml +1 -1
- {sdev-0.7.10 → sdev-0.7.12}/sdev/cli/__init__.py +200 -178
- {sdev-0.7.10 → sdev-0.7.12}/sdev/modules/serial_notebook.py +14 -2
- {sdev-0.7.10 → sdev-0.7.12/sdev.egg-info}/PKG-INFO +1 -1
- {sdev-0.7.10 → sdev-0.7.12}/setup.py +1 -1
- {sdev-0.7.10 → sdev-0.7.12}/LICENSE +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/MANIFEST.in +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/README.md +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/__init__.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/cli/__main__.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/deprecated/demoboard.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/modules/__init__.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/modules/mcp_server.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/modules/serial_rw.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev/modules/serial_rw_server.py +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev.egg-info/SOURCES.txt +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev.egg-info/dependency_links.txt +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev.egg-info/entry_points.txt +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev.egg-info/requires.txt +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/sdev.egg-info/top_level.txt +0 -0
- {sdev-0.7.10 → sdev-0.7.12}/setup.cfg +0 -0
|
@@ -9,6 +9,8 @@ sdev CLI 入口:
|
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
|
+
import contextlib
|
|
13
|
+
import fcntl
|
|
12
14
|
import hashlib
|
|
13
15
|
import itertools
|
|
14
16
|
import json
|
|
@@ -43,6 +45,21 @@ def _sdev_cache_dir() -> str:
|
|
|
43
45
|
return d
|
|
44
46
|
|
|
45
47
|
|
|
48
|
+
@contextlib.contextmanager
|
|
49
|
+
def _list_global_lock():
|
|
50
|
+
"""
|
|
51
|
+
sdev list 进程级全局锁。
|
|
52
|
+
本工具不支持并发 list;这里用阻塞锁串行化,避免并发扫描互相干扰。
|
|
53
|
+
"""
|
|
54
|
+
lock_path = os.path.join(_sdev_cache_dir(), "list.lock")
|
|
55
|
+
with open(lock_path, "w", encoding="utf-8") as f:
|
|
56
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_EX)
|
|
57
|
+
try:
|
|
58
|
+
yield
|
|
59
|
+
finally:
|
|
60
|
+
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
|
|
61
|
+
|
|
62
|
+
|
|
46
63
|
def _run_parallel_with_spinners(specs: List[Tuple[str, Any]]) -> List[Any]:
|
|
47
64
|
"""
|
|
48
65
|
多行并行 spinner:
|
|
@@ -243,7 +260,7 @@ def _build_parser() -> argparse.ArgumentParser:
|
|
|
243
260
|
list_p.add_argument(
|
|
244
261
|
"scope_or_filter",
|
|
245
262
|
nargs="?",
|
|
246
|
-
help="local
|
|
263
|
+
help="默认仅本地扫描;可显式传 local/remote,或直接给第一个筛选条件(如 type=xc01)",
|
|
247
264
|
)
|
|
248
265
|
list_p.add_argument(
|
|
249
266
|
"-f",
|
|
@@ -453,187 +470,192 @@ def _ts() -> str:
|
|
|
453
470
|
|
|
454
471
|
|
|
455
472
|
def _handle_list(args: argparse.Namespace) -> int:
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
def
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
results.append((port, r))
|
|
494
|
-
if r.get("available"):
|
|
495
|
-
fast_exit = True
|
|
496
|
-
alive_ports = [port]
|
|
497
|
-
break
|
|
498
|
-
else:
|
|
499
|
-
for fut in as_completed(futs):
|
|
500
|
-
results.append(fut.result())
|
|
501
|
-
results.sort(key=lambda x: x[0])
|
|
502
|
-
for port, r in results:
|
|
503
|
-
if r.get("available"):
|
|
504
|
-
alive_ports.append(port)
|
|
505
|
-
finally:
|
|
506
|
-
ex.shutdown(wait=not fast_exit)
|
|
507
|
-
|
|
508
|
-
def _model_one(port: str) -> Tuple[str, str]:
|
|
473
|
+
with _list_global_lock():
|
|
474
|
+
extra_filters = list(getattr(args, "filters", None) or [])
|
|
475
|
+
scope_or_filter = getattr(args, "scope_or_filter", None)
|
|
476
|
+
|
|
477
|
+
scope: Optional[str]
|
|
478
|
+
initial_filter_parts: list[str] = []
|
|
479
|
+
if scope_or_filter in ("local", "remote"):
|
|
480
|
+
scope = scope_or_filter
|
|
481
|
+
elif scope_or_filter is None:
|
|
482
|
+
# 默认不访问网络,仅扫描本地串口设备。
|
|
483
|
+
scope = "local"
|
|
484
|
+
else:
|
|
485
|
+
# 传入筛选条件时,依然沿用默认仅本地扫描。
|
|
486
|
+
scope = "local"
|
|
487
|
+
initial_filter_parts.append(scope_or_filter)
|
|
488
|
+
|
|
489
|
+
filters = _parse_list_filters(initial_filter_parts + extra_filters)
|
|
490
|
+
fast = getattr(args, "fast", False)
|
|
491
|
+
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
492
|
+
|
|
493
|
+
scan_local = scope != "remote"
|
|
494
|
+
scan_remote = scope != "local"
|
|
495
|
+
|
|
496
|
+
# 本地与远程扫描分别封装为任务,由 spinner 展示进度。
|
|
497
|
+
def _scan_local_task(ports: List[str]) -> List[Dict[str, Any]]:
|
|
498
|
+
entries: List[Dict[str, Any]] = []
|
|
499
|
+
if not ports:
|
|
500
|
+
return entries
|
|
501
|
+
|
|
502
|
+
def _check_one(port: str) -> Tuple[str, Dict[str, Any]]:
|
|
503
|
+
r = check_port_alive_nonblock(port, baudrate=115200, timeout=1.5)
|
|
504
|
+
return (port, r)
|
|
505
|
+
|
|
506
|
+
alive_ports: List[str] = []
|
|
507
|
+
results: List[Tuple[str, Dict[str, Any]]] = []
|
|
508
|
+
fast_exit = False
|
|
509
|
+
ex = ThreadPoolExecutor(max_workers=max(len(ports), 1))
|
|
509
510
|
try:
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return entries
|
|
531
|
-
|
|
532
|
-
def _scan_remote_task() -> List[Dict[str, Any]]:
|
|
533
|
-
entries: List[Dict[str, Any]] = []
|
|
534
|
-
hosts = get_remote_hosts()
|
|
535
|
-
# print(f"[debug] remote hosts found by discovery: {hosts}")
|
|
536
|
-
if not hosts:
|
|
537
|
-
return entries
|
|
538
|
-
for host, service_port in hosts:
|
|
539
|
-
# print(f"[debug] fetching boards from {host}:{service_port}...")
|
|
540
|
-
boards = fetch_boards_from_host(host, service_port, timeout=10.0)
|
|
541
|
-
# print(f"[debug] host {host} returned {len(boards)} boards")
|
|
542
|
-
for b in boards:
|
|
543
|
-
if not b.get("available"):
|
|
544
|
-
continue
|
|
545
|
-
serial_dev = (b.get("device") or b.get("port") or "").strip()
|
|
546
|
-
if not serial_dev:
|
|
547
|
-
continue
|
|
548
|
-
baud = int(b.get("baudrate") or 115200)
|
|
549
|
-
device_type = "unknown"
|
|
511
|
+
futs = {ex.submit(_check_one, p): p for p in ports}
|
|
512
|
+
if fast:
|
|
513
|
+
for fut in as_completed(futs):
|
|
514
|
+
port, r = fut.result()
|
|
515
|
+
results.append((port, r))
|
|
516
|
+
if r.get("available"):
|
|
517
|
+
fast_exit = True
|
|
518
|
+
alive_ports = [port]
|
|
519
|
+
break
|
|
520
|
+
else:
|
|
521
|
+
for fut in as_completed(futs):
|
|
522
|
+
results.append(fut.result())
|
|
523
|
+
results.sort(key=lambda x: x[0])
|
|
524
|
+
for port, r in results:
|
|
525
|
+
if r.get("available"):
|
|
526
|
+
alive_ports.append(port)
|
|
527
|
+
finally:
|
|
528
|
+
ex.shutdown(wait=not fast_exit)
|
|
529
|
+
|
|
530
|
+
def _model_one(port: str) -> Tuple[str, str]:
|
|
550
531
|
try:
|
|
551
|
-
with SerialNotebook(device=
|
|
552
|
-
|
|
532
|
+
with SerialNotebook(device=port, baudrate=115200) as nb:
|
|
533
|
+
return (port, nb.get_model_type(timeout=2.0, verbose=False) or "unknown")
|
|
553
534
|
except Exception as e:
|
|
554
|
-
|
|
555
|
-
|
|
535
|
+
return (port, f"error: {e}")
|
|
536
|
+
|
|
537
|
+
if alive_ports:
|
|
538
|
+
model_results: List[Tuple[str, str]] = []
|
|
539
|
+
with ThreadPoolExecutor(max_workers=max(len(alive_ports), 1)) as ex2:
|
|
540
|
+
futs = {ex2.submit(_model_one, p): p for p in alive_ports}
|
|
541
|
+
for fut in as_completed(futs):
|
|
542
|
+
model_results.append(fut.result())
|
|
543
|
+
model_results.sort(key=lambda x: x[0])
|
|
544
|
+
for port, device_type in model_results:
|
|
545
|
+
entries.append({
|
|
546
|
+
"host": "localhost",
|
|
547
|
+
"device": port,
|
|
548
|
+
"device_id": _device_id("localhost", port),
|
|
549
|
+
"device_type": device_type if not device_type.startswith("error:") else "unknown",
|
|
550
|
+
"last_update": now,
|
|
551
|
+
})
|
|
552
|
+
return entries
|
|
553
|
+
|
|
554
|
+
def _scan_remote_task() -> List[Dict[str, Any]]:
|
|
555
|
+
entries: List[Dict[str, Any]] = []
|
|
556
|
+
hosts = get_remote_hosts()
|
|
557
|
+
# print(f"[debug] remote hosts found by discovery: {hosts}")
|
|
558
|
+
if not hosts:
|
|
559
|
+
return entries
|
|
560
|
+
for host, service_port in hosts:
|
|
561
|
+
# print(f"[debug] fetching boards from {host}:{service_port}...")
|
|
562
|
+
boards = fetch_boards_from_host(host, service_port, timeout=10.0)
|
|
563
|
+
# print(f"[debug] host {host} returned {len(boards)} boards")
|
|
564
|
+
for b in boards:
|
|
565
|
+
if not b.get("available"):
|
|
566
|
+
continue
|
|
567
|
+
serial_dev = (b.get("device") or b.get("port") or "").strip()
|
|
568
|
+
if not serial_dev:
|
|
569
|
+
continue
|
|
570
|
+
baud = int(b.get("baudrate") or 115200)
|
|
556
571
|
device_type = "unknown"
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
572
|
+
try:
|
|
573
|
+
with SerialNotebook(device=serial_dev, baudrate=baud, host=host, port=service_port) as nb:
|
|
574
|
+
device_type = nb.get_model_type(timeout=3.0, verbose=False) or "unknown"
|
|
575
|
+
except Exception as e:
|
|
576
|
+
# 如果报错,说明连接或者握手阶段就出问题了
|
|
577
|
+
# print(f"[debug] failed to get model type from {host}:{serial_dev}: {e}")
|
|
578
|
+
device_type = "unknown"
|
|
579
|
+
entries.append({
|
|
580
|
+
"host": host,
|
|
581
|
+
"device": serial_dev,
|
|
582
|
+
"device_id": _device_id(host, serial_dev),
|
|
583
|
+
"device_type": device_type if not device_type.startswith("error:") else "unknown",
|
|
584
|
+
"last_update": now,
|
|
585
|
+
})
|
|
586
|
+
return entries
|
|
587
|
+
|
|
588
|
+
specs: List[Tuple[str, Any]] = []
|
|
589
|
+
local_entries: List[Dict[str, Any]] = []
|
|
590
|
+
remote_entries: List[Dict[str, Any]] = []
|
|
591
|
+
idx_local: Optional[int] = None
|
|
592
|
+
idx_remote: Optional[int] = None
|
|
593
|
+
|
|
594
|
+
if scan_local and scan_remote:
|
|
595
|
+
ports = scan_serial_ports()
|
|
596
|
+
label = f"[main] scanning {len(ports)} local port(s) and discovering remote hosts..."
|
|
597
|
+
print(label)
|
|
598
|
+
# 本地和远程作为两个并行任务:
|
|
599
|
+
idx_local = 0
|
|
600
|
+
idx_remote = 1
|
|
601
|
+
specs.append(("[local] scanning local ports", lambda p=ports: _scan_local_task(p)))
|
|
602
|
+
specs.append(("[remote] discovering remote hosts", _scan_remote_task))
|
|
603
|
+
elif scan_local:
|
|
604
|
+
ports = scan_serial_ports()
|
|
605
|
+
label = f"[main] scanning {len(ports)} local port(s)..."
|
|
606
|
+
print(label)
|
|
607
|
+
idx_local = 0
|
|
608
|
+
specs.append(("[local] scanning local ports", lambda p=ports: _scan_local_task(p)))
|
|
609
|
+
else:
|
|
610
|
+
# 仅远程
|
|
611
|
+
print("[main] discovering remote hosts...")
|
|
612
|
+
idx_remote = 0
|
|
613
|
+
specs.append(("[remote] discovering remote hosts", _scan_remote_task))
|
|
614
|
+
|
|
615
|
+
if specs:
|
|
616
|
+
results = _run_parallel_with_spinners(specs)
|
|
617
|
+
if idx_local is not None:
|
|
618
|
+
local_entries = results[idx_local] or []
|
|
619
|
+
if idx_remote is not None:
|
|
620
|
+
remote_entries = results[idx_remote] or []
|
|
621
|
+
|
|
622
|
+
entries = list(local_entries) + list(remote_entries)
|
|
623
|
+
|
|
624
|
+
print("[main] all scanning finished.")
|
|
625
|
+
|
|
626
|
+
# 保存本次扫描到的全部设备到缓存,便于后续 sdev run / set default 解析 host/port
|
|
627
|
+
_write_list_cache(list(entries))
|
|
628
|
+
|
|
629
|
+
# 应用筛选(仅影响本次打印)
|
|
630
|
+
if "type" in filters:
|
|
631
|
+
entries = [e for e in entries if (e.get("device_type") or "").lower() == filters["type"].lower()]
|
|
632
|
+
if "host" in filters:
|
|
633
|
+
entries = [e for e in entries if (e.get("host") or "").lower() == filters["host"].lower()]
|
|
634
|
+
|
|
635
|
+
# 默认展示所有探测到的设备,即便型号识别为 unknown。
|
|
636
|
+
# entries = [e for e in entries if (e.get("device_type") or "").lower() != "unknown"]
|
|
637
|
+
if fast and len(entries) > 1:
|
|
638
|
+
entries = entries[:1]
|
|
639
|
+
|
|
640
|
+
# 表格输出
|
|
641
|
+
col_host = 14
|
|
642
|
+
col_port = 16
|
|
643
|
+
col_id = 10
|
|
644
|
+
col_type = 14
|
|
645
|
+
col_time = 20
|
|
646
|
+
header = f"{'HOST':<{col_host}} {'PORT':<{col_port}} {'DEVICE ID':<{col_id}} {'DEVICE TYPE':<{col_type}} {'LAST UPDATE':<{col_time}}"
|
|
647
|
+
sep = "-" * (col_host + col_port + col_id + col_type + col_time + 4)
|
|
648
|
+
print(header)
|
|
649
|
+
print(sep)
|
|
650
|
+
for e in entries:
|
|
651
|
+
print(
|
|
652
|
+
f"{e.get('host', ''):<{col_host}} "
|
|
653
|
+
f"{e.get('device', ''):<{col_port}} "
|
|
654
|
+
f"{e.get('device_id', ''):<{col_id}} "
|
|
655
|
+
f"{e.get('device_type', 'unknown'):<{col_type}} "
|
|
656
|
+
f"{e.get('last_update', ''):<{col_time}}"
|
|
657
|
+
)
|
|
658
|
+
return 0
|
|
637
659
|
|
|
638
660
|
|
|
639
661
|
def _handle_set(args: argparse.Namespace) -> int:
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
|
|
2
1
|
import sys
|
|
3
2
|
import time
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
4
5
|
from loguru import logger
|
|
5
6
|
|
|
7
|
+
|
|
8
|
+
def _normalize_device_tree_model(value: Optional[str]) -> Optional[str]:
|
|
9
|
+
"""
|
|
10
|
+
规范化从 /proc/device-tree/model 读到的文本:去除 NUL 与首尾空白。
|
|
11
|
+
部分板卡会在型号末尾带 \\0,会导致 sdev list type=... 无法匹配。
|
|
12
|
+
"""
|
|
13
|
+
if value is None:
|
|
14
|
+
return None
|
|
15
|
+
cleaned = value.replace("\x00", "").strip()
|
|
16
|
+
return cleaned or None
|
|
17
|
+
|
|
6
18
|
from .serial_rw import SerialLine, CTRL_C
|
|
7
19
|
|
|
8
20
|
GREY = "\033[90m"
|
|
@@ -230,7 +242,7 @@ class SerialNotebook():
|
|
|
230
242
|
verbose=verbose,
|
|
231
243
|
)
|
|
232
244
|
if lines and len(lines) >= 2:
|
|
233
|
-
return lines[-2]
|
|
245
|
+
return _normalize_device_tree_model(lines[-2])
|
|
234
246
|
except Exception:
|
|
235
247
|
pass
|
|
236
248
|
return None
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|