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.
- {sdev-0.4.4/sdev.egg-info → sdev-0.4.6}/PKG-INFO +8 -8
- {sdev-0.4.4 → sdev-0.4.6}/README.md +7 -7
- {sdev-0.4.4 → sdev-0.4.6}/pyproject.toml +1 -1
- {sdev-0.4.4 → sdev-0.4.6}/sdev/__init__.py +1 -1
- sdev-0.4.6/sdev/class_wrapper.py +164 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev/core.py +37 -123
- sdev-0.4.6/sdev/demoboard/functional.py +177 -0
- {sdev-0.4.4 → sdev-0.4.6/sdev.egg-info}/PKG-INFO +8 -8
- {sdev-0.4.4 → sdev-0.4.6}/setup.py +1 -1
- sdev-0.4.4/sdev/class_wrapper.py +0 -85
- sdev-0.4.4/sdev/demoboard/functional.py +0 -146
- {sdev-0.4.4 → sdev-0.4.6}/LICENSE +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/MANIFEST.in +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev/cli_wrapper.py +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev/demoboard/__init__.py +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev/relay/__init__.py +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev/relay/functional.py +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev.egg-info/SOURCES.txt +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev.egg-info/dependency_links.txt +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev.egg-info/entry_points.txt +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev.egg-info/requires.txt +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/sdev.egg-info/top_level.txt +0 -0
- {sdev-0.4.4 → sdev-0.4.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdev
|
|
3
|
-
Version: 0.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
|
-
|
|
37
|
+
串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
|
|
38
38
|
|
|
39
|
-
|
|
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=" #",
|
|
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
|
-
-
|
|
111
|
+
- **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
|
|
112
112
|
|
|
113
113
|
## 依赖
|
|
114
114
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# SDEV
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
|
|
4
4
|
|
|
5
|
-
|
|
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=" #",
|
|
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
|
-
-
|
|
77
|
+
- **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
|
|
78
78
|
|
|
79
79
|
## 依赖
|
|
80
80
|
|
|
@@ -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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
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
|
-
|
|
37
|
+
串口开发板控制器:基于后台异步读缓冲、提供快速存活检测、多进程串口互斥锁、以及人性化的命令回显。
|
|
38
38
|
|
|
39
|
-
|
|
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=" #",
|
|
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
|
-
-
|
|
111
|
+
- **回显样式**:`Demoboard` 提供了增强的视觉体验。用户输入的命令和提示符符号 `>` 以黄色高亮显示,命令执行结果以白色显示,而普通的板端日志则以深灰色背景化。
|
|
112
112
|
|
|
113
113
|
## 依赖
|
|
114
114
|
|
sdev-0.4.4/sdev/class_wrapper.py
DELETED
|
@@ -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
|
|
File without changes
|
|
File without changes
|