sdev 0.4.4__tar.gz → 0.4.6__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.4.4
3
+ Version: 0.4.6
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -34,9 +34,9 @@ Dynamic: requires-python
34
34
 
35
35
  # SDEV
36
36
 
37
- 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
37
+ 串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
38
38
 
39
- **注意**:同一串口同一时间只能被一个 sdev 进程使用。请勿并行执行多条 `sdev shell ...` 或快速连续执行,否则可能报「串口已被其他 sdev 进程占用」或挂死;需等前一条命令结束后再执行下一条。
39
+ **注意**:`sdev` 内置了跨进程串口锁(flock),支持多个进程同时尝试连接同一串口。后启动的进程会阻塞等待,直到前一个进程释放串口,从而避免了串口冲突或挂死。
40
40
 
41
41
  ## 安装
42
42
 
@@ -95,20 +95,20 @@ with Demoboard("/dev/ttyUSB0", 115200) as board:
95
95
  |------|------|
96
96
  | `shell(cmd, prompt_flag=" #", timeout=None, stream=False)` | 清缓冲 -> 发送命令 -> 等待命令回显 -> 等待提示符。返回输出列表或生成器。 |
97
97
  | `execute_command(cmd, flag, timeout=None, stream=False)` | `shell` 方法的别名,用于兼容旧版代码。 |
98
- | `check_alive(uboot_flag="uboot#", prompt_flag=" #", response_timeout=3)` | 存活检测:处理 U-Boot 挂起、发送 Ctrl-C 唤醒并等待提示符。 |
98
+ | `check_alive(uboot_flag="uboot#", prompt_flag=" #", timeout=2.0)` | 鲁棒存活检测:自动处理串口残留输出、U-Boot 挂起、发送 Ctrl-C 唤醒。具备乐观探测能力,在系统稳定时可秒级完成检测。 |
99
99
 
100
- ### SerialDevice (底层)
100
+ ### SerialDevice (底层驱动)
101
+
102
+ 底层串口基础类,仅负责 I/O 和锁管理,不包含任何 UI/显示逻辑。
101
103
 
102
104
  | 方法 / 属性 | 说明 |
103
105
  |-------------|------|
104
- | `STABLE_FLAG` | 类常量 `" #"`,常用作默认提示符。 |
105
106
  | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程。 |
106
107
  | `send(text)` | 发送一行(自动加换行)。 |
107
108
  | `scan(end_flag, timeout=None)` | 从缓存中持续读取直到目标 flag,或读到 timeout 结束。 |
108
- | `display(lines, flag=None, flag_type=None)` | 对传入的行按 flag 类型回显(着色)并 yield 原始行。 |
109
109
  | `clear()` | 清空读缓冲。 |
110
110
 
111
- - **回显着色**:匹配到 `flag` 的行为绿色,命令回显行为青色,其余灰色;支持设备折行后的跨行匹配。
111
+ - **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
112
112
 
113
113
  ## 依赖
114
114
 
@@ -1,8 +1,8 @@
1
1
  # SDEV
2
2
 
3
- 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
3
+ 串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
4
4
 
5
- **注意**:同一串口同一时间只能被一个 sdev 进程使用。请勿并行执行多条 `sdev shell ...` 或快速连续执行,否则可能报「串口已被其他 sdev 进程占用」或挂死;需等前一条命令结束后再执行下一条。
5
+ **注意**:`sdev` 内置了跨进程串口锁(flock),支持多个进程同时尝试连接同一串口。后启动的进程会阻塞等待,直到前一个进程释放串口,从而避免了串口冲突或挂死。
6
6
 
7
7
  ## 安装
8
8
 
@@ -61,20 +61,20 @@ with Demoboard("/dev/ttyUSB0", 115200) as board:
61
61
  |------|------|
62
62
  | `shell(cmd, prompt_flag=" #", timeout=None, stream=False)` | 清缓冲 -> 发送命令 -> 等待命令回显 -> 等待提示符。返回输出列表或生成器。 |
63
63
  | `execute_command(cmd, flag, timeout=None, stream=False)` | `shell` 方法的别名,用于兼容旧版代码。 |
64
- | `check_alive(uboot_flag="uboot#", prompt_flag=" #", response_timeout=3)` | 存活检测:处理 U-Boot 挂起、发送 Ctrl-C 唤醒并等待提示符。 |
64
+ | `check_alive(uboot_flag="uboot#", prompt_flag=" #", timeout=2.0)` | 鲁棒存活检测:自动处理串口残留输出、U-Boot 挂起、发送 Ctrl-C 唤醒。具备乐观探测能力,在系统稳定时可秒级完成检测。 |
65
65
 
66
- ### SerialDevice (底层)
66
+ ### SerialDevice (底层驱动)
67
+
68
+ 底层串口基础类,仅负责 I/O 和锁管理,不包含任何 UI/显示逻辑。
67
69
 
68
70
  | 方法 / 属性 | 说明 |
69
71
  |-------------|------|
70
- | `STABLE_FLAG` | 类常量 `" #"`,常用作默认提示符。 |
71
72
  | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程。 |
72
73
  | `send(text)` | 发送一行(自动加换行)。 |
73
74
  | `scan(end_flag, timeout=None)` | 从缓存中持续读取直到目标 flag,或读到 timeout 结束。 |
74
- | `display(lines, flag=None, flag_type=None)` | 对传入的行按 flag 类型回显(着色)并 yield 原始行。 |
75
75
  | `clear()` | 清空读缓冲。 |
76
76
 
77
- - **回显着色**:匹配到 `flag` 的行为绿色,命令回显行为青色,其余灰色;支持设备折行后的跨行匹配。
77
+ - **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
78
78
 
79
79
  ## 依赖
80
80
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdev"
7
- version = "0.4.4"
7
+ version = "0.4.6"
8
8
  description = "串口控制器工具包"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -14,7 +14,7 @@ from typing import Literal
14
14
  from .core import SerialDevice
15
15
  from .class_wrapper import Demoboard, Relay
16
16
 
17
- __version__ = "0.4.4"
17
+ __version__ = "0.4.6"
18
18
  __author__ = "klrc"
19
19
  __email__ = "1440698245@qq.com"
20
20
 
@@ -0,0 +1,164 @@
1
+ """
2
+ 面向对象封装:
3
+
4
+ - Demoboard:继承 SerialDevice,并增加显示回显(display)、日志记录和高层方法(shell, check_alive);
5
+ - Relay:继承 SerialDevice,仅保留基础串口能力。
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ from typing import Generator, Iterable, List, Literal, Optional, Union
13
+
14
+ from loguru import logger
15
+
16
+ from .core import SerialDevice
17
+ from .demoboard import check_alive as _check_alive_func
18
+ from .demoboard import shell as _shell_func
19
+
20
+ # 配色方案(仅用于 Demoboard 显示)
21
+ _GREY = "\033[90m"
22
+ _GREEN = "\033[32m"
23
+ _YELLOW = "\033[33m"
24
+ _RESET = "\033[0m"
25
+
26
+ # ANSI 颜色转义正则
27
+ _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
28
+
29
+
30
+ class Demoboard(SerialDevice):
31
+ """
32
+ Demoboard 设备:
33
+ - 具备完整的显示回显和着色能力;
34
+ - 提供 shell() / check_alive() 等交互式方法。
35
+ """
36
+
37
+ def __init__(self, port: str, baudrate: int = 115200, check_alive: bool = True):
38
+ super().__init__(port, baudrate)
39
+ self._auto_check_alive = check_alive
40
+ self._display = True
41
+ self._last_output_ends_with_newline = True
42
+
43
+ def connect(self) -> None:
44
+ """连接并自动执行 check_alive。"""
45
+ if self._is_connected:
46
+ return
47
+ super().connect()
48
+ if self._auto_check_alive:
49
+ self.check_alive()
50
+
51
+ # --- 高级显示与日志逻辑 (重写基类接口) ---
52
+
53
+ def _write_raw(self, text: str) -> None:
54
+ """重写:增加终端输出。"""
55
+ if text:
56
+ self._last_output_ends_with_newline = text.endswith("\n")
57
+ sys.stdout.write(text)
58
+ sys.stdout.flush()
59
+
60
+ def _ensure_newline(self) -> None:
61
+ """重写:增加自动补全换行逻辑。"""
62
+ if not self._last_output_ends_with_newline:
63
+ sys.stdout.write("\n")
64
+ sys.stdout.flush()
65
+ self._last_output_ends_with_newline = True
66
+
67
+ def info(self, msg: str) -> None:
68
+ self._ensure_newline()
69
+ logger.info(msg)
70
+
71
+ def success(self, msg: str) -> None:
72
+ self._ensure_newline()
73
+ logger.success(msg)
74
+
75
+ def warning(self, msg: str) -> None:
76
+ self._ensure_newline()
77
+ logger.warning(msg)
78
+
79
+ def error(self, msg: str) -> None:
80
+ self._ensure_newline()
81
+ logger.error(msg)
82
+
83
+ def _echo_normal(self, line: str) -> None:
84
+ self._write_raw(f"{_GREY}{line}{_RESET}")
85
+
86
+ def display(
87
+ self,
88
+ lines: Iterable[str],
89
+ flag: Optional[str] = None,
90
+ flag_type: Optional[Literal["end_flag", "prompt"]] = None,
91
+ raw_color: bool = False,
92
+ ) -> Generator[str, None, None]:
93
+ """重写:增加局部着色回显。"""
94
+ for line in lines:
95
+ if self._display:
96
+ if raw_color:
97
+ self._write_raw(line)
98
+ elif flag is None:
99
+ self._echo_normal(line)
100
+ else:
101
+ # 高亮模式:仅对匹配到的 flag 部分加色
102
+ plain = _ANSI_ESCAPE_RE.sub("", line)
103
+ if flag and flag in plain:
104
+ parts = plain.split(flag, 1)
105
+ # 如果是 prompt (输入命令回显),使用 Yellow;如果是 end_flag (提示符),使用 Green
106
+ color = _GREEN if flag_type == "end_flag" else _YELLOW
107
+ formatted = f"{_GREY}{parts[0]}{_RESET}{color}{flag}{_RESET}{_GREY}{parts[1]}{_RESET}"
108
+ self._write_raw(formatted)
109
+ else:
110
+ self._echo_normal(plain)
111
+ yield line
112
+
113
+ # --- 高层业务方法 ---
114
+
115
+ def shell(
116
+ self,
117
+ cmd: str,
118
+ *,
119
+ prompt_flag: str = " #",
120
+ timeout: Optional[float] = None,
121
+ stream: bool = False,
122
+ ) -> Union[List[str], Generator[str, None, None]]:
123
+ return _shell_func(self, cmd, prompt_flag=prompt_flag, timeout=timeout, stream=stream)
124
+
125
+ def execute_command(
126
+ self,
127
+ cmd: str,
128
+ flag: str = " #",
129
+ timeout: Optional[float] = None,
130
+ stream: bool = False,
131
+ ) -> Union[List[str], Generator[str, None, None]]:
132
+ """兼容旧版签名的 shell 别名。"""
133
+ return self.shell(cmd, prompt_flag=flag, timeout=timeout, stream=stream)
134
+
135
+ def check_alive(
136
+ self,
137
+ *,
138
+ uboot_flag: str = "uboot#",
139
+ prompt_flag: str = " #",
140
+ timeout: float = 2.0,
141
+ ) -> None:
142
+ _check_alive_func(
143
+ self,
144
+ uboot_flag=uboot_flag,
145
+ prompt_flag=prompt_flag,
146
+ timeout=timeout,
147
+ )
148
+
149
+ def __exit__(self, exc_type, exc_value, traceback) -> None:
150
+ super().__exit__(exc_type, exc_value, traceback)
151
+ self._ensure_newline()
152
+
153
+
154
+ class Relay(SerialDevice):
155
+ """
156
+ 继电器设备:仅继承基础串口能力,不具备 Demoboard 的显示回显功能。
157
+ """
158
+
159
+ def __init__(self, port: str, baudrate: int = 115200, **kwargs):
160
+ kwargs.pop("check_alive", None)
161
+ super().__init__(port, baudrate)
162
+
163
+
164
+ __all__ = ["Demoboard", "Relay"]
@@ -1,8 +1,8 @@
1
1
  """
2
- 串口设备核心:连接、后台按行读、发送、按 flag 扫描/回显、清空缓存。
2
+ 串口设备核心:连接、后台按行读、发送、按 flag 扫描、清空缓存。
3
3
  跨进程串口互斥:同一 port 的 connect 通过 fcntl.flock 阻塞等待,依次执行。
4
4
 
5
- 高层功能(shell、check_alive 等)放在上层模块中实现,例如 `sdev.demoboard.functional`。
5
+ 底层类,不包含任何 UI/显示逻辑。
6
6
  """
7
7
 
8
8
  import fcntl
@@ -11,7 +11,7 @@ import queue
11
11
  import re
12
12
  import threading
13
13
  import time
14
- from typing import Generator, Iterable, Literal, Optional
14
+ from typing import Generator, Iterable, Optional
15
15
 
16
16
  from loguru import logger
17
17
  import serial
@@ -31,8 +31,6 @@ def _port_lock_path(port: str) -> str:
31
31
  def _acquire_port_lock(port: str):
32
32
  """
33
33
  对 port 持有跨进程排他锁(阻塞直到拿到),返回打开的 fd,调用方负责 close。
34
-
35
- 注意:这里是 sdev 的全局锁机制入口,请不要在上层重复实现文件锁。
36
34
  """
37
35
  path = _port_lock_path(port)
38
36
  fd = os.open(path, os.O_RDWR | os.O_CREAT, 0o600)
@@ -46,9 +44,10 @@ def _acquire_port_lock(port: str):
46
44
  now = time.monotonic()
47
45
  if now - last_log >= 5.0:
48
46
  waited = now - start
47
+ # 底层锁提示,保持简单
49
48
  logger.warning(
50
49
  f"Waiting for cross-process serial lock on {port} "
51
- f"for {waited:.1f} seconds; another sdev instance may be using this port."
50
+ f"for {waited:.1f} seconds..."
52
51
  )
53
52
  last_log = now
54
53
  time.sleep(0.1)
@@ -68,32 +67,12 @@ def _release_port_lock(fd: Optional[int]) -> None:
68
67
 
69
68
 
70
69
  def _content_contains_ctrl_c(text: str) -> bool:
71
- """是否包含设备回显的 Ctrl+C(\\x03 或 ^C、^C\\x00 等)。"""
70
+ """是否包含设备回显的 Ctrl+C"""
72
71
  n = text.replace("\x00", "")
73
72
  return CTRL_C in n or "^C" in n
74
73
 
75
74
 
76
- def _line_matches_flag(line: str, flag: str) -> bool:
77
- """行(rstrip 后)是否以 flag 结尾;CTRL_C 兼容 \\x03 / \"^C\" 等回显形式。"""
78
- stripped = line.rstrip()
79
- if flag != CTRL_C:
80
- return stripped.endswith(flag)
81
- normalized = stripped.strip().replace("\x00", "")
82
- return normalized == CTRL_C or normalized == "^C" or normalized.endswith("^C") or stripped.endswith(flag)
83
-
84
-
85
- _GREY = "\033[90m"
86
- _SENT = "\033[36m"
87
- _GREEN = "\033[32m"
88
- _RESET = "\033[0m"
89
-
90
- # 匹配已有的 ANSI 颜色转义,用于在高亮模式下去掉原始颜色
91
- _ANSI_ESCAPE_RE = re.compile(r"\x1b\[[0-9;]*m")
92
-
93
-
94
75
  class SerialDevice:
95
- STABLE_FLAG = " #"
96
-
97
76
  def __init__(self, port: str, baudrate: int = 115200):
98
77
  self.port = port
99
78
  self.baudrate = baudrate
@@ -102,8 +81,6 @@ class SerialDevice:
102
81
  self._reader_thread: Optional[threading.Thread] = None
103
82
  self._is_connected = False
104
83
  self._stop_reading = False
105
- self._display = True
106
- # 跨进程串口锁 fd,connect 时持锁,disconnect 时释放
107
84
  self._lock_fd: Optional[int] = None
108
85
 
109
86
  # 扫描相关通用参数
@@ -111,10 +88,6 @@ class SerialDevice:
111
88
  self._max_lines_per_prompt = 4
112
89
 
113
90
  def _serial_reader(self) -> None:
114
- """
115
- 后台 daemon:readline → UTF-8 解码 → put(line);
116
- 解码失败则跳过该行。disconnect 时 _stop_reading + put(None) 唤醒阻塞 get()。
117
- """
118
91
  while not self._stop_reading and self._serial is not None and self._serial.is_open:
119
92
  raw = self._serial.readline()
120
93
  if raw:
@@ -126,13 +99,9 @@ class SerialDevice:
126
99
  continue
127
100
 
128
101
  def connect(self) -> None:
129
- """
130
- 打开串口并启动后台读取线程;已连接则直接 return。
131
- 同一 port 的多次 connect 会因跨进程锁而阻塞排队。
132
- """
133
102
  if self._is_connected:
134
103
  return
135
- self._lock_fd = _acquire_port_lock(self.port) # 阻塞直到拿到串口锁
104
+ self._lock_fd = _acquire_port_lock(self.port)
136
105
  try:
137
106
  self._serial = serial.Serial(self.port, self.baudrate, timeout=self._scan_timeout)
138
107
  if not self._serial.is_open:
@@ -147,13 +116,9 @@ class SerialDevice:
147
116
  raise
148
117
 
149
118
  def disconnect(self) -> None:
150
- """
151
- 关闭串口并停止后台线程;幂等。
152
- """
153
119
  if not self._is_connected:
154
120
  return
155
121
  self._stop_reading = True
156
- # 唤醒可能阻塞在 _buffer.get() 的 scan() 调用
157
122
  self._buffer.put(None)
158
123
  if self._reader_thread is not None and self._reader_thread.is_alive():
159
124
  self._reader_thread.join()
@@ -166,33 +131,26 @@ class SerialDevice:
166
131
  self._is_connected = False
167
132
 
168
133
  def send(self, prompt: str) -> None:
169
- """发送 prompt + 换行并 flush;prompt 不得含 \\n/\\r。"""
170
134
  if not self._is_connected or self._serial is None:
171
135
  raise RuntimeError("not connected")
172
- assert prompt is not None, "prompt 不得为 None"
173
- assert "\n" not in prompt and "\r" not in prompt, "send(prompt) 不得包含换行符"
136
+ assert prompt is not None
174
137
  self._serial.write((prompt + "\n").encode("utf-8"))
175
138
  self._serial.flush()
176
139
 
177
140
  def _scan_raw(self, timeout: Optional[float] = None) -> Generator[str, None, None]:
178
- """
179
- 从 _buffer 逐行 yield。
180
- 有 timeout 时按「无响应超时」语义处理:超过 timeout 一直没有新行则抛 TimeoutError;
181
- 收到 None 结束。
182
- """
183
141
  if not self._is_connected:
184
142
  raise RuntimeError("not connected")
185
143
  last_activity = time.monotonic()
186
144
  scan_timeout = self._scan_timeout if timeout is not None else None
187
145
 
188
- while True: # timeout loop
146
+ while True:
189
147
  try:
190
148
  line = self._buffer.get(timeout=scan_timeout) if scan_timeout else self._buffer.get()
191
149
  except queue.Empty:
192
150
  if timeout is not None and time.monotonic() - last_activity > timeout:
193
151
  raise TimeoutError("scan deadline exceeded (no response)")
194
152
  continue
195
- if line is None: # _serial_reader sentinel
153
+ if line is None:
196
154
  return
197
155
  last_activity = time.monotonic()
198
156
  yield line
@@ -203,16 +161,6 @@ class SerialDevice:
203
161
  timeout: Optional[float] = None,
204
162
  replace_with_acc: bool = False,
205
163
  ) -> Generator[str, None, None]:
206
- """
207
- 从缓存中持续读取直到目标 flag,或读到 timeout 结束。
208
-
209
- - end_flag 为 None 时:持续按行读取,直到内部 timeout 为止,TimeoutError 视为正常结束(return)。
210
- - end_flag 非 None:内部维护一个 acc_cache 保留最近几行,并拼成 acc_line(无换行),
211
- 以支持跨行匹配;命中后:
212
- - replace_with_acc=True:yield 一行拼接后的 acc_line + \"\\n\";
213
- - replace_with_acc=False:逐行 yield acc_cache 中的原始行。
214
- - 如果 end_flag 是 CTRL_C,则通过 _content_contains_ctrl_c() 进行兼容匹配。
215
- """
216
164
  if end_flag is None:
217
165
  try:
218
166
  for line in self._scan_raw(timeout):
@@ -224,7 +172,6 @@ class SerialDevice:
224
172
  for line in self._scan_raw(timeout):
225
173
  acc_cache.append(line)
226
174
  if len(acc_cache) > self._max_lines_per_prompt:
227
- # 缓存过多则把最旧的一行交给调用方
228
175
  yield acc_cache.pop(0)
229
176
 
230
177
  acc_line = "".join([x.rstrip("\r\n") for x in acc_cache])
@@ -237,54 +184,7 @@ class SerialDevice:
237
184
  yield x
238
185
  break
239
186
 
240
- def _echo_prompt(self, line: str) -> None:
241
- print(f"{_SENT}{line}{_RESET}", end="", flush=True)
242
-
243
- def _echo_normal(self, line: str) -> None:
244
- print(f"{_GREY}{line}{_RESET}", end="", flush=True)
245
-
246
- def _echo_flag(self, line: str) -> None:
247
- print(f"{_GREEN}{line}{_RESET}", end="", flush=True)
248
-
249
- def display(
250
- self,
251
- lines: Iterable[str],
252
- flag: Optional[str] = None,
253
- flag_type: Optional[Literal["end_flag", "prompt"]] = None,
254
- raw_color = False,
255
- ) -> Generator[str, None, None]:
256
- """
257
- 对传入的行按 flag_type 回显并 yield 原始行。
258
-
259
- - flag/flag_type 为 None 时:保持原始输出(包括设备自身带的颜色),不做处理。
260
- - flag_type=\"end_flag\":匹配行高亮为绿色,其它行灰色;在高亮模式下会去除原始 ANSI 颜色。
261
- - flag_type=\"prompt\":匹配行高亮为青色,其它行灰色;在高亮模式下会去除原始 ANSI 颜色。
262
- """
263
- for line in lines:
264
- if self._display:
265
- if raw_color:
266
- # 不做任何颜色包裹,保留设备原始颜色
267
- print(line, end="", flush=True)
268
- elif flag is None:
269
- self._echo_normal(line)
270
- else:
271
- # 高亮模式:去除原始颜色后按 flag_type 统一着色
272
- plain = _ANSI_ESCAPE_RE.sub("", line)
273
- find_flag = _line_matches_flag(plain, flag)
274
- if find_flag:
275
- if flag_type == "end_flag":
276
- self._echo_flag(plain)
277
- elif flag_type == "prompt":
278
- self._echo_prompt(plain)
279
- else:
280
- self._echo_normal(plain)
281
- yield line
282
-
283
187
  def clear(self) -> None:
284
- """
285
- 清空 _buffer 中已入队行。
286
- 取到 None 则 put(None) 后 return(保留 sentinel)。
287
- """
288
188
  while True:
289
189
  try:
290
190
  item = self._buffer.get_nowait()
@@ -294,22 +194,36 @@ class SerialDevice:
294
194
  except queue.Empty:
295
195
  return
296
196
 
197
+ # --- 基础日志与输出接口 (供子类扩展或 functional 使用) ---
198
+
199
+ def _write_raw(self, text: str) -> None:
200
+ """底层默认不向终端输出任何内容。"""
201
+ pass
202
+
203
+ def _ensure_newline(self) -> None:
204
+ """底层默认不处理换行。"""
205
+ pass
206
+
207
+ def info(self, msg: str) -> None:
208
+ """底层默认仅通过 logger 记录。"""
209
+ logger.info(msg)
210
+
211
+ def success(self, msg: str) -> None:
212
+ logger.success(msg)
213
+
214
+ def warning(self, msg: str) -> None:
215
+ logger.warning(msg)
216
+
217
+ def error(self, msg: str) -> None:
218
+ logger.error(msg)
219
+
220
+ def display(self, lines: Iterable[str], **kwargs) -> Generator[str, None, None]:
221
+ """底层默认仅 yield 数据,不进行任何回显。"""
222
+ yield from lines
223
+
297
224
  def __enter__(self) -> "SerialDevice":
298
225
  self.connect()
299
226
  return self
300
227
 
301
228
  def __exit__(self, exc_type, exc_value, traceback) -> None:
302
229
  self.disconnect()
303
- print()
304
-
305
-
306
- if __name__ == "__main__":
307
- # 简单示例:打印一条命令输出(不做存活检测等高层逻辑)
308
- with SerialDevice("/dev/ttyUSB0", 115200) as board:
309
- board.send("ls")
310
- for _ in board.display(
311
- board.scan(SerialDevice.STABLE_FLAG, timeout=3),
312
- SerialDevice.STABLE_FLAG,
313
- "end_flag",
314
- ):
315
- pass
@@ -0,0 +1,177 @@
1
+ """
2
+ Demoboard 相关的高层功能:
3
+
4
+ - shell(): clear + send + scan + display,执行一条命令并按提示符结束;
5
+ - check_alive(): 通过 Ctrl-C / uboot / reboot / 提示符 等 flag 检查板子是否存活。
6
+
7
+ 注意:这里假设底层设备实现了 SerialDevice 接口:
8
+ - clear()
9
+ - send(cmd: str)
10
+ - scan(end_flag: Optional[str], timeout: Optional[float], replace_with_acc: bool)
11
+ - display(lines, flag: Optional[str], flag_type: Optional[str])
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from itertools import chain
18
+ from typing import Generator, Iterable, List, Optional, Union
19
+ from loguru import logger
20
+
21
+ from ..core import CTRL_C, SerialDevice
22
+
23
+
24
+ def _scan_and_display(
25
+ device: SerialDevice,
26
+ end_flag: str,
27
+ flag_type: str,
28
+ timeout: Optional[float],
29
+ replace_with_acc: bool,
30
+ ) -> Generator[str, None, None]:
31
+ """内部小工具:封装一段 scan + display,返回的是 scan 的原始行。"""
32
+ lines = device.scan(end_flag=end_flag, timeout=timeout, replace_with_acc=replace_with_acc)
33
+ for line in device.display(lines, flag=end_flag, flag_type=flag_type): # pragma: no branch
34
+ yield line
35
+
36
+
37
+ def shell(
38
+ device: SerialDevice,
39
+ cmd: str,
40
+ prompt_flag: str = " #",
41
+ timeout: Optional[float] = None,
42
+ stream: bool = False,
43
+ clear: bool = True,
44
+ ) -> Union[List[str], Generator[str, None, None]]:
45
+ """
46
+ 在 demoboard 上执行一条命令:
47
+ - clear: 清理残留输出,避免错位;
48
+ - send: 发送命令;
49
+ - 第一阶段:scan(cmd) + display(prompt 高亮),直到命令回显完整出现;
50
+ - 第二阶段:scan(prompt_flag) + display(end_flag 高亮),直到提示符行出现。
51
+
52
+ 返回的是 scan 的原始行(设备输出的原始内容),不是 display 的显示结果:
53
+ - stream=False:返回这些原始行的 list(两阶段全部合并);
54
+ - stream=True:返回 yielding 原始行的 generator,不预先把结果拉成 list。
55
+ """
56
+ if clear:
57
+ device.clear()
58
+
59
+ # 第一段:把「命令本身的回显」当作 prompt 高亮
60
+ part1 = []
61
+ if cmd is not None:
62
+ # 营造类似 Python 交互式的结构:空一行,另起一行打印 > 符号
63
+ device._ensure_newline()
64
+ device._write_raw(f"\n\033[33m>\033[0m ") # 黄色的 > 符号
65
+ device.send(cmd)
66
+ part1 = _scan_and_display(
67
+ device,
68
+ end_flag=cmd,
69
+ flag_type="prompt",
70
+ timeout=timeout,
71
+ replace_with_acc=True,
72
+ )
73
+
74
+ # 第二段:直到提示符
75
+ part2 = _scan_and_display(
76
+ device,
77
+ end_flag=prompt_flag,
78
+ flag_type="end_flag",
79
+ timeout=timeout,
80
+ replace_with_acc=False,
81
+ )
82
+ merged = (line for line in chain(part1, part2))
83
+
84
+ if stream:
85
+ # 按要求返回「原始 generator」
86
+ return merged
87
+ return list(merged)
88
+
89
+ def check_alive(
90
+ device: SerialDevice,
91
+ uboot_flag: str = "uboot#",
92
+ prompt_flag: str = " #",
93
+ timeout: float = 2.0,
94
+ ) -> None:
95
+ """
96
+ 检查 demoboard 是否「醒着」且有 shell 提示符。
97
+
98
+ 语义(鲁棒性逻辑):
99
+ 1. 乐观探测 (Fast Path):如果已经在提示符状态或串口静默,直接探测以节省时间。
100
+ 2. 标准流程:检查 timeout 内是否有输出;如果有,循环等待直到没有输出;
101
+ 3. 输入 CTRL-C 并检查输出:
102
+ - 如果包含 uboot_flag,输入 'reboot' 并递归调用 check_alive;
103
+ - 否则,检查是否包含 prompt_flag。如果不包含,提示并抛出错误;
104
+ 4. 如果一切正常,视为 check 成功。
105
+ """
106
+
107
+ # 1. 乐观探测 (Fast Path)
108
+ try:
109
+ # 极短采样 (0.3s)
110
+ sample = list(shell(device, None, prompt_flag=None, timeout=0.3, clear=False))
111
+ sample_out = "".join(sample)
112
+
113
+ # 场景 A: 已经在提示符处
114
+ if prompt_flag in sample_out:
115
+ if uboot_flag in sample_out:
116
+ device.warning(f"Found uboot flag '{uboot_flag}', sending 'reboot' and re-checking...")
117
+ device.send("reboot")
118
+ return check_alive(device, uboot_flag, prompt_flag, timeout)
119
+ return
120
+
121
+ # 场景 B: 串口当前没动静,尝试主动探测
122
+ if not sample_out:
123
+ device.send(CTRL_C)
124
+ # 探测回显 (0.5s)
125
+ probe = list(shell(device, None, prompt_flag=None, timeout=0.5, clear=False))
126
+ probe_out = "".join(probe)
127
+ if prompt_flag in probe_out:
128
+ if uboot_flag in probe_out:
129
+ device.warning(f"Found uboot flag '{uboot_flag}', sending 'reboot' and re-checking...")
130
+ device.send("reboot")
131
+ return check_alive(device, uboot_flag, prompt_flag, timeout)
132
+ return
133
+ except Exception as e:
134
+ logger.debug(f"Optimistic probe encountered error: {e}")
135
+
136
+ # 2. 标准流程:检查并等待输出停止
137
+ while True:
138
+ try:
139
+ # shell(device, None, ...) 不发送命令,仅执行 scan(None) 直到 silence timeout。
140
+ lines = list(shell(device, None, prompt_flag=None, timeout=timeout, clear=False))
141
+ if not lines:
142
+ break
143
+ device.warning("Detecting serial output, waiting for it to stop...")
144
+ except (TimeoutError, Exception) as e:
145
+ logger.debug(f"Stop-output loop encountered error: {e}")
146
+ break
147
+
148
+ # 3. 发送 CTRL-C 并获取输出
149
+ device.send(CTRL_C)
150
+ lines = list(shell(device, None, prompt_flag=None, timeout=timeout, clear=False))
151
+ output = "".join(lines)
152
+
153
+ # 4. 如果输出包含 uboot_flag,输入 reboot 并返回递归的 check_alive
154
+ if uboot_flag in output:
155
+ device.warning(f"Found uboot flag '{uboot_flag}', sending 'reboot' and re-checking...")
156
+ device.send("reboot")
157
+ return check_alive(device, uboot_flag, prompt_flag, timeout)
158
+
159
+ # 5. 检查 prompt_flag
160
+ if prompt_flag not in output:
161
+ msg = f"Prompt flag '{prompt_flag}' not found in output. Serial port might not be connected or device is unresponsive. Please check."
162
+ device.error(msg)
163
+ raise RuntimeError(msg)
164
+
165
+ # 6. 成功
166
+ device.success("Check alive passed.")
167
+
168
+
169
+ if __name__ == "__main__":
170
+ # 简单测试:demoboard shell 功能
171
+ import os
172
+
173
+ port = os.environ.get("SDEV_PORT", "/dev/ttyUSB0")
174
+ with SerialDevice(port) as board:
175
+ check_alive(board)
176
+ shell(board, "cat /proc/meminfo")
177
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdev
3
- Version: 0.4.4
3
+ Version: 0.4.6
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -34,9 +34,9 @@ Dynamic: requires-python
34
34
 
35
35
  # SDEV
36
36
 
37
- 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
37
+ 串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
38
38
 
39
- **注意**:同一串口同一时间只能被一个 sdev 进程使用。请勿并行执行多条 `sdev shell ...` 或快速连续执行,否则可能报「串口已被其他 sdev 进程占用」或挂死;需等前一条命令结束后再执行下一条。
39
+ **注意**:`sdev` 内置了跨进程串口锁(flock),支持多个进程同时尝试连接同一串口。后启动的进程会阻塞等待,直到前一个进程释放串口,从而避免了串口冲突或挂死。
40
40
 
41
41
  ## 安装
42
42
 
@@ -95,20 +95,20 @@ with Demoboard("/dev/ttyUSB0", 115200) as board:
95
95
  |------|------|
96
96
  | `shell(cmd, prompt_flag=" #", timeout=None, stream=False)` | 清缓冲 -> 发送命令 -> 等待命令回显 -> 等待提示符。返回输出列表或生成器。 |
97
97
  | `execute_command(cmd, flag, timeout=None, stream=False)` | `shell` 方法的别名,用于兼容旧版代码。 |
98
- | `check_alive(uboot_flag="uboot#", prompt_flag=" #", response_timeout=3)` | 存活检测:处理 U-Boot 挂起、发送 Ctrl-C 唤醒并等待提示符。 |
98
+ | `check_alive(uboot_flag="uboot#", prompt_flag=" #", timeout=2.0)` | 鲁棒存活检测:自动处理串口残留输出、U-Boot 挂起、发送 Ctrl-C 唤醒。具备乐观探测能力,在系统稳定时可秒级完成检测。 |
99
99
 
100
- ### SerialDevice (底层)
100
+ ### SerialDevice (底层驱动)
101
+
102
+ 底层串口基础类,仅负责 I/O 和锁管理,不包含任何 UI/显示逻辑。
101
103
 
102
104
  | 方法 / 属性 | 说明 |
103
105
  |-------------|------|
104
- | `STABLE_FLAG` | 类常量 `" #"`,常用作默认提示符。 |
105
106
  | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程。 |
106
107
  | `send(text)` | 发送一行(自动加换行)。 |
107
108
  | `scan(end_flag, timeout=None)` | 从缓存中持续读取直到目标 flag,或读到 timeout 结束。 |
108
- | `display(lines, flag=None, flag_type=None)` | 对传入的行按 flag 类型回显(着色)并 yield 原始行。 |
109
109
  | `clear()` | 清空读缓冲。 |
110
110
 
111
- - **回显着色**:匹配到 `flag` 的行为绿色,命令回显行为青色,其余灰色;支持设备折行后的跨行匹配。
111
+ - **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
112
112
 
113
113
  ## 依赖
114
114
 
@@ -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.4.4",
8
+ version="0.4.6",
9
9
  author="klrc",
10
10
  author_email="144069824@qq.com",
11
11
  description="串口控制器工具包",
@@ -1,85 +0,0 @@
1
- """
2
- 面向对象封装:
3
-
4
- - Demoboard:继承 SerialDevice,并把 demoboard.functional 里的函数挂成方法;
5
- - Relay:占位类,后续扩展继电器相关串口控制。
6
- """
7
-
8
- from __future__ import annotations
9
-
10
- from typing import Generator, List, Optional, Union
11
-
12
- from .core import SerialDevice
13
- from .demoboard import check_alive as _check_alive_func
14
- from .demoboard import shell as _shell_func
15
-
16
-
17
- class Demoboard(SerialDevice):
18
- """
19
- Demoboard 设备:
20
- - 保留 SerialDevice 的所有底层能力;
21
- - 提供 shell() / check_alive() 等高层方法。
22
- """
23
-
24
- def __init__(self, port: str, baudrate: int = 115200, check_alive: bool = True):
25
- super().__init__(port, baudrate)
26
- self._auto_check_alive = check_alive
27
-
28
- def connect(self) -> None:
29
- """连接并自动执行 check_alive。"""
30
- if self._is_connected:
31
- return
32
- super().connect()
33
- if self._auto_check_alive:
34
- self.check_alive()
35
-
36
- def execute_command(
37
- self,
38
- cmd: str,
39
- flag: str = " #",
40
- timeout: Optional[float] = None,
41
- stream: bool = False,
42
- ) -> Union[List[str], Generator[str, None, None]]:
43
- """alias for shell"""
44
- return self.shell(cmd, prompt_flag=flag, timeout=timeout, stream=stream)
45
-
46
- def shell(
47
- self,
48
- cmd: str,
49
- *,
50
- prompt_flag: str = " #",
51
- timeout: Optional[float] = None,
52
- stream: bool = False,
53
- ) -> Union[List[str], Generator[str, None, None]]:
54
- return _shell_func(self, cmd, prompt_flag=prompt_flag, timeout=timeout, stream=stream)
55
-
56
- def check_alive(
57
- self,
58
- *,
59
- uboot_flag: str = "uboot#",
60
- prompt_flag: str = " #",
61
- response_timeout: float = 3,
62
- ) -> None:
63
- _check_alive_func(
64
- self,
65
- uboot_flag=uboot_flag,
66
- prompt_flag=prompt_flag,
67
- response_timeout=response_timeout,
68
- )
69
-
70
-
71
- class Relay(SerialDevice):
72
- """
73
- 继电器设备占位类:
74
- - 继承 SerialDevice;
75
- - 具体继电器控制逻辑后续在 sdev.relay.functional 中补充。
76
- """
77
-
78
- def __init__(self, port: str, baudrate: int = 115200, **kwargs):
79
- # 吞掉不支持的参数,确保与 sdev() 工厂兼容
80
- kwargs.pop("check_alive", None)
81
- super().__init__(port, baudrate)
82
-
83
-
84
- __all__ = ["Demoboard", "Relay"]
85
-
@@ -1,146 +0,0 @@
1
- """
2
- Demoboard 相关的高层功能:
3
-
4
- - shell(): clear + send + scan + display,执行一条命令并按提示符结束;
5
- - check_alive(): 通过 Ctrl-C / uboot / reboot / 提示符 等 flag 检查板子是否存活。
6
-
7
- 注意:这里假设底层设备实现了 SerialDevice 接口:
8
- - clear()
9
- - send(cmd: str)
10
- - scan(end_flag: Optional[str], timeout: Optional[float], replace_with_acc: bool)
11
- - display(lines, flag: Optional[str], flag_type: Optional[str])
12
- """
13
-
14
- from __future__ import annotations
15
-
16
- import time
17
- from itertools import chain
18
- from typing import Generator, Iterable, List, Optional, Union
19
- from loguru import logger
20
-
21
- from ..core import CTRL_C, SerialDevice
22
-
23
-
24
- def _scan_and_display(
25
- device: SerialDevice,
26
- end_flag: str,
27
- flag_type: str,
28
- timeout: Optional[float],
29
- replace_with_acc: bool,
30
- ) -> Generator[str, None, None]:
31
- """内部小工具:封装一段 scan + display,返回的是 scan 的原始行。"""
32
- lines = device.scan(end_flag=end_flag, timeout=timeout, replace_with_acc=replace_with_acc)
33
- for line in device.display(lines, flag=end_flag, flag_type=flag_type): # pragma: no branch
34
- yield line
35
-
36
-
37
- def shell(
38
- device: SerialDevice,
39
- cmd: str,
40
- prompt_flag: str = " #",
41
- timeout: Optional[float] = None,
42
- stream: bool = False,
43
- clear: bool = True,
44
- ) -> Union[List[str], Generator[str, None, None]]:
45
- """
46
- 在 demoboard 上执行一条命令:
47
- - clear: 清理残留输出,避免错位;
48
- - send: 发送命令;
49
- - 第一阶段:scan(cmd) + display(prompt 高亮),直到命令回显完整出现;
50
- - 第二阶段:scan(prompt_flag) + display(end_flag 高亮),直到提示符行出现。
51
-
52
- 返回的是 scan 的原始行(设备输出的原始内容),不是 display 的显示结果:
53
- - stream=False:返回这些原始行的 list(两阶段全部合并);
54
- - stream=True:返回 yielding 原始行的 generator,不预先把结果拉成 list。
55
- """
56
- if clear:
57
- device.clear()
58
-
59
- # 第一段:把「命令本身的回显」当作 prompt 高亮
60
- part1 = []
61
- if cmd is not None:
62
- device.send(cmd)
63
- part1 = _scan_and_display(
64
- device,
65
- end_flag=cmd,
66
- flag_type="prompt",
67
- timeout=timeout,
68
- replace_with_acc=True,
69
- )
70
-
71
- # 第二段:直到提示符
72
- part2 = _scan_and_display(
73
- device,
74
- end_flag=prompt_flag,
75
- flag_type="end_flag",
76
- timeout=timeout,
77
- replace_with_acc=False,
78
- )
79
- merged = (line for line in chain(part1, part2))
80
-
81
- if stream:
82
- # 按要求返回「原始 generator」
83
- return merged
84
- return list(merged)
85
-
86
- def check_alive(
87
- device: SerialDevice,
88
- uboot_flag: str = "uboot#",
89
- prompt_flag: str = " #",
90
- response_timeout: float = 3,
91
- ) -> None:
92
- """
93
- 检查 demoboard 是否「醒着」且有 shell 提示符。
94
-
95
- 语义(带超时的熔断):
96
- 1. 发送 Ctrl-C 并读取一段输出:
97
- - 如果输出中出现 uboot_flag(如 "uboot#"),认为当前在 U-Boot;
98
- 2. 若在 U-Boot:
99
- - 发送 "reboot";
100
- - 等待 awaken_flag(如 "Process ... done" 中的 "Process")出现,超时则抛 TimeoutError;
101
- 3. 最后,无论是否经过 reboot,统一等待 shell 提示符 prompt_flag(如 "#"),超时抛 TimeoutError。
102
- """
103
-
104
- # 1. Ctrl-C,判定控制台是否响应
105
- under_reboot = False
106
- try:
107
- lines = shell(device, CTRL_C, prompt_flag, timeout=response_timeout)
108
- except TimeoutError:
109
- wait_iter = 0
110
- while True:
111
- device.send(CTRL_C)
112
- lines = shell(device, None, None, timeout=response_timeout, clear=False)
113
- if len(lines) > 0:
114
- under_reboot = True
115
- break
116
- wait_iter += 1
117
- if wait_iter > 5:
118
- logger.warning("CTRL-C test failed, please check the connection")
119
- break
120
-
121
- # 2. 检查是否被中止进入uboot
122
- for line in lines:
123
- if uboot_flag in line:
124
- logger.warning("uboot flag found, rebooting")
125
- device.send("reboot")
126
- under_reboot = True
127
- break
128
-
129
- # 3. 进入启动流程
130
- if under_reboot:
131
- try:
132
- lines = shell(device, None, None, timeout=response_timeout)
133
- except TimeoutError:
134
- pass # 等待启动彻底完成
135
- return check_alive(device, uboot_flag, prompt_flag, response_timeout)
136
-
137
-
138
- if __name__ == "__main__":
139
- # 简单测试:demoboard shell 功能
140
- import os
141
-
142
- port = os.environ.get("SDEV_PORT", "/dev/ttyUSB0")
143
- with SerialDevice(port) as board:
144
- check_alive(board)
145
- shell(board, "cat /proc/meminfo")
146
-
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