sdev 0.7.9__tar.gz → 0.7.11__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdev
3
- Version: 0.7.9
3
+ Version: 0.7.11
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdev"
7
- version = "0.7.9"
7
+ version = "0.7.11"
8
8
  description = "串口控制器工具包"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -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=仅本地, remote=仅远程, 或第一个筛选条件(如 type=xc01)",
263
+ help="默认仅本地扫描;可显式传 local/remote,或直接给第一个筛选条件(如 type=xc01)",
247
264
  )
248
265
  list_p.add_argument(
249
266
  "-f",
@@ -453,23 +470,28 @@ def _ts() -> str:
453
470
 
454
471
 
455
472
  def _handle_list(args: argparse.Namespace) -> int:
456
- extra_filters = list(getattr(args, "filters", None) or [])
457
- scope_or_filter = getattr(args, "scope_or_filter", None)
458
-
459
- scope: Optional[str]
460
- initial_filter_parts: list[str] = []
461
- if scope_or_filter in (None, "local", "remote"):
462
- scope = scope_or_filter
463
- else:
464
- scope = None
465
- initial_filter_parts.append(scope_or_filter)
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)
466
488
 
467
- filters = _parse_list_filters(initial_filter_parts + extra_filters)
468
- fast = getattr(args, "fast", False)
469
- now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
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")
470
492
 
471
- scan_local = scope != "remote"
472
- scan_remote = scope != "local"
493
+ scan_local = scope != "remote"
494
+ scan_remote = scope != "local"
473
495
 
474
496
  # 本地与远程扫描分别封装为任务,由 spinner 展示进度。
475
497
  def _scan_local_task(ports: List[str]) -> List[Dict[str, Any]]:
@@ -563,77 +585,77 @@ def _handle_list(args: argparse.Namespace) -> int:
563
585
  })
564
586
  return entries
565
587
 
566
- specs: List[Tuple[str, Any]] = []
567
- local_entries: List[Dict[str, Any]] = []
568
- remote_entries: List[Dict[str, Any]] = []
569
- idx_local: Optional[int] = None
570
- idx_remote: Optional[int] = None
571
-
572
- if scan_local and scan_remote:
573
- ports = scan_serial_ports()
574
- label = f"[main] scanning {len(ports)} local port(s) and discovering remote hosts..."
575
- print(label)
576
- # 本地和远程作为两个并行任务:
577
- idx_local = 0
578
- idx_remote = 1
579
- specs.append(("[local] scanning local ports", lambda p=ports: _scan_local_task(p)))
580
- specs.append(("[remote] discovering remote hosts", _scan_remote_task))
581
- elif scan_local:
582
- ports = scan_serial_ports()
583
- label = f"[main] scanning {len(ports)} local port(s)..."
584
- print(label)
585
- idx_local = 0
586
- specs.append(("[local] scanning local ports", lambda p=ports: _scan_local_task(p)))
587
- else:
588
- # 仅远程
589
- print("[main] discovering remote hosts...")
590
- idx_remote = 0
591
- specs.append(("[remote] discovering remote hosts", _scan_remote_task))
592
-
593
- if specs:
594
- results = _run_parallel_with_spinners(specs)
595
- if idx_local is not None:
596
- local_entries = results[idx_local] or []
597
- if idx_remote is not None:
598
- remote_entries = results[idx_remote] or []
599
-
600
- entries = list(local_entries) + list(remote_entries)
601
-
602
- print("[main] all scanning finished.")
603
-
604
- # 保存本次扫描到的全部设备到缓存,便于后续 sdev run / set default 解析 host/port
605
- _write_list_cache(list(entries))
606
-
607
- # 应用筛选(仅影响本次打印)
608
- if "type" in filters:
609
- entries = [e for e in entries if (e.get("device_type") or "").lower() == filters["type"].lower()]
610
- if "host" in filters:
611
- entries = [e for e in entries if (e.get("host") or "").lower() == filters["host"].lower()]
612
-
613
- # 默认展示所有探测到的设备,即便型号识别为 unknown。
614
- # entries = [e for e in entries if (e.get("device_type") or "").lower() != "unknown"]
615
- if fast and len(entries) > 1:
616
- entries = entries[:1]
617
-
618
- # 表格输出
619
- col_host = 14
620
- col_port = 16
621
- col_id = 10
622
- col_type = 14
623
- col_time = 20
624
- header = f"{'HOST':<{col_host}} {'PORT':<{col_port}} {'DEVICE ID':<{col_id}} {'DEVICE TYPE':<{col_type}} {'LAST UPDATE':<{col_time}}"
625
- sep = "-" * (col_host + col_port + col_id + col_type + col_time + 4)
626
- print(header)
627
- print(sep)
628
- for e in entries:
629
- print(
630
- f"{e.get('host', ''):<{col_host}} "
631
- f"{e.get('device', ''):<{col_port}} "
632
- f"{e.get('device_id', ''):<{col_id}} "
633
- f"{e.get('device_type', 'unknown'):<{col_type}} "
634
- f"{e.get('last_update', ''):<{col_time}}"
635
- )
636
- return 0
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"
@@ -41,7 +53,7 @@ class SerialNotebook():
41
53
  def __exit__(self, exc_type, exc_value, traceback):
42
54
  self._core.disconnect()
43
55
 
44
- def run(self, cmd: str, end_flag=None, allow_fold=True, max_fold_lines=4, strip_echo=True, stream=False, verbose=True, overall_timeout=None, response_timeout=None, keep_type=False):
56
+ def run(self, cmd: str, end_flag=None, allow_fold=True, max_fold_lines=10, strip_echo=True, stream=False, verbose=True, overall_timeout=None, response_timeout=None, keep_type=False):
45
57
  # 先清理残留输出,避免前一次命令的尾巴影响本次结果。
46
58
  assert cmd is not None
47
59
  self._core.drain_buffer()
@@ -101,9 +113,6 @@ class SerialNotebook():
101
113
  except KeyboardInterrupt:
102
114
  # 若外层未使用 stream(即调用方期望一次性拿到结果),则在此捕获本地 Ctrl-C,
103
115
  # 并尽量向远端会话发送一次 Ctrl-C,再收集一段时间的输出后正常结束。
104
- if stream:
105
- # 对于 stream=True 的调用,保留原始行为,让调用方决定如何处理 Ctrl-C。
106
- raise
107
116
 
108
117
  # 打印一次用户中断提示(黄色),避免与正常输出混淆。
109
118
  if show_log:
@@ -233,7 +242,7 @@ class SerialNotebook():
233
242
  verbose=verbose,
234
243
  )
235
244
  if lines and len(lines) >= 2:
236
- return lines[-2].strip()
245
+ return _normalize_device_tree_model(lines[-2])
237
246
  except Exception:
238
247
  pass
239
248
  return None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdev
3
- Version: 0.7.9
3
+ Version: 0.7.11
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="sdev",
8
- version="0.7.9",
8
+ version="0.7.11",
9
9
  author="klrc",
10
10
  author_email="144069824@qq.com",
11
11
  description="串口控制器工具包",
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