sdev 0.2.2__tar.gz → 0.3.0__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.3.0/PKG-INFO ADDED
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: sdev
3
+ Version: 0.3.0
4
+ Summary: 串口控制器工具包
5
+ Home-page: https://github.com/klrc/sdev
6
+ Author: klrc
7
+ Author-email: klrc <144069824@qq.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/klrc/sdev
10
+ Project-URL: Repository, https://github.com/klrc/sdev
11
+ Project-URL: Bug Tracker, https://github.com/klrc/sdev/issues
12
+ Keywords: serial,controller,hardware,embedded
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: System :: Hardware
25
+ Requires-Python: >=3.7
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: pyserial>=3.5
29
+ Requires-Dist: loguru>=0.6.0
30
+ Dynamic: author
31
+ Dynamic: home-page
32
+ Dynamic: license-file
33
+ Dynamic: requires-python
34
+
35
+ # SDEV
36
+
37
+ 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
38
+
39
+ ## 安装
40
+
41
+ ```bash
42
+ pip install sdev
43
+ ```
44
+
45
+ ## 命令行
46
+
47
+ 安装后可使用 `sdev` 命令,在默认串口上执行单条命令并回显:
48
+
49
+ ```bash
50
+ sdev shell "lsmod | grep -E 'mmz|nnp|vdsp'"
51
+ sdev -p /dev/ttyUSB1 -b 115200 shell "ls"
52
+ sdev --no-check-alive shell "echo hello"
53
+ ```
54
+
55
+ - `-p` / `--port`:串口路径(默认取环境变量 `SDEV_PORT` 或 `/dev/ttyUSB0`)
56
+ - `-b` / `--baudrate`:波特率(默认取 `SDEV_BAUDRATE` 或 `115200`)
57
+ - `--flag`:提示符结束标志(默认 `" #"`)
58
+ - `--timeout`:等待输出超时秒数
59
+ - `--no-check-alive`:连接后不执行存活检测
60
+
61
+ ## 快速使用(Python)
62
+
63
+ ```python
64
+ from sdev import SerialDevice
65
+
66
+ with SerialDevice("/dev/ttyUSB0", 115200) as board:
67
+ # connect 时默认会 check_alive(STABLE_FLAG),未接串口会轮询等待
68
+ out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
69
+ print(out)
70
+ ```
71
+
72
+ 不自动做存活检测时:
73
+
74
+ ```python
75
+ board = SerialDevice("/dev/ttyUSB0", 115200, check_alive=False)
76
+ board.connect()
77
+ board.wait_for_flag("~ #", timeout=20) # 可选:等到指定提示符
78
+ board.execute_command("ls")
79
+ board.disconnect()
80
+ ```
81
+
82
+ ## 主要接口
83
+
84
+ | 方法 / 属性 | 说明 |
85
+ |-------------|------|
86
+ | `SerialDevice.STABLE_FLAG` | 类常量 `" #"`,常用作稳定提示符 |
87
+ | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程;`connect` 可选执行 `check_alive(STABLE_FLAG)` |
88
+ | `send(text)` | 发送一行(自动加换行);`text` 不得含 `\n` / `\r` |
89
+ | `execute_command(cmd, flag=" #", stream=False, timeout=None)` | 清缓冲 → 发 `cmd` → 先等命令回显再等含 `flag` 的提示行;返回列表或生成器;`cmd=Ctrl+C` 时兼容 ^C 回显 |
90
+ | `check_alive(stable_flag)` | 有输出则 `wait_for_flag(stable_flag)` 后返回;否则发 Ctrl-C 再等;超时则打 warning 并轮询直到有输出 |
91
+ | `wait_for_flag(flag, timeout)` | 消费输出直到出现 `flag` 或超时 |
92
+ | `send_interrupt(timeout, end_flag=" #")` | 发 Ctrl-C 并等含 `end_flag` 的提示行;超时抛 `TimeoutError` |
93
+ | `clear()` | 清空读缓冲(保留 sentinel),一般由 `execute_command` 内部调用 |
94
+
95
+ - 回显:匹配到 flag 的行为绿色,命令回显行为青色,其余灰色;设备折行会拼行再判 flag。
96
+
97
+ ## 依赖
98
+
99
+ - Python >= 3.7
100
+ - pyserial >= 3.5
101
+ - loguru >= 0.6.0
sdev-0.3.0/README.md ADDED
@@ -0,0 +1,67 @@
1
+ # SDEV
2
+
3
+ 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
4
+
5
+ ## 安装
6
+
7
+ ```bash
8
+ pip install sdev
9
+ ```
10
+
11
+ ## 命令行
12
+
13
+ 安装后可使用 `sdev` 命令,在默认串口上执行单条命令并回显:
14
+
15
+ ```bash
16
+ sdev shell "lsmod | grep -E 'mmz|nnp|vdsp'"
17
+ sdev -p /dev/ttyUSB1 -b 115200 shell "ls"
18
+ sdev --no-check-alive shell "echo hello"
19
+ ```
20
+
21
+ - `-p` / `--port`:串口路径(默认取环境变量 `SDEV_PORT` 或 `/dev/ttyUSB0`)
22
+ - `-b` / `--baudrate`:波特率(默认取 `SDEV_BAUDRATE` 或 `115200`)
23
+ - `--flag`:提示符结束标志(默认 `" #"`)
24
+ - `--timeout`:等待输出超时秒数
25
+ - `--no-check-alive`:连接后不执行存活检测
26
+
27
+ ## 快速使用(Python)
28
+
29
+ ```python
30
+ from sdev import SerialDevice
31
+
32
+ with SerialDevice("/dev/ttyUSB0", 115200) as board:
33
+ # connect 时默认会 check_alive(STABLE_FLAG),未接串口会轮询等待
34
+ out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
35
+ print(out)
36
+ ```
37
+
38
+ 不自动做存活检测时:
39
+
40
+ ```python
41
+ board = SerialDevice("/dev/ttyUSB0", 115200, check_alive=False)
42
+ board.connect()
43
+ board.wait_for_flag("~ #", timeout=20) # 可选:等到指定提示符
44
+ board.execute_command("ls")
45
+ board.disconnect()
46
+ ```
47
+
48
+ ## 主要接口
49
+
50
+ | 方法 / 属性 | 说明 |
51
+ |-------------|------|
52
+ | `SerialDevice.STABLE_FLAG` | 类常量 `" #"`,常用作稳定提示符 |
53
+ | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程;`connect` 可选执行 `check_alive(STABLE_FLAG)` |
54
+ | `send(text)` | 发送一行(自动加换行);`text` 不得含 `\n` / `\r` |
55
+ | `execute_command(cmd, flag=" #", stream=False, timeout=None)` | 清缓冲 → 发 `cmd` → 先等命令回显再等含 `flag` 的提示行;返回列表或生成器;`cmd=Ctrl+C` 时兼容 ^C 回显 |
56
+ | `check_alive(stable_flag)` | 有输出则 `wait_for_flag(stable_flag)` 后返回;否则发 Ctrl-C 再等;超时则打 warning 并轮询直到有输出 |
57
+ | `wait_for_flag(flag, timeout)` | 消费输出直到出现 `flag` 或超时 |
58
+ | `send_interrupt(timeout, end_flag=" #")` | 发 Ctrl-C 并等含 `end_flag` 的提示行;超时抛 `TimeoutError` |
59
+ | `clear()` | 清空读缓冲(保留 sentinel),一般由 `execute_command` 内部调用 |
60
+
61
+ - 回显:匹配到 flag 的行为绿色,命令回显行为青色,其余灰色;设备折行会拼行再判 flag。
62
+
63
+ ## 依赖
64
+
65
+ - Python >= 3.7
66
+ - pyserial >= 3.5
67
+ - loguru >= 0.6.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdev"
7
- version = "0.2.2"
7
+ version = "0.3.0"
8
8
  description = "串口控制器工具包"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -32,6 +32,9 @@ dependencies = [
32
32
  "loguru>=0.6.0",
33
33
  ]
34
34
 
35
+ [project.scripts]
36
+ sdev = "sdev.cli_wrapper:main"
37
+
35
38
  [project.urls]
36
39
  Homepage = "https://github.com/klrc/sdev"
37
40
  Repository = "https://github.com/klrc/sdev"
@@ -4,10 +4,10 @@ SDEV - 串口控制器工具包
4
4
  Demoboard:串口连接、后台异步读、发送、按行/按 flag 读、回显、组合命令、存活检测。
5
5
  """
6
6
 
7
- from .demoboard import Demoboard
7
+ from .core import SerialDevice
8
8
 
9
- __version__ = "0.2.2"
9
+ __version__ = "0.3.0"
10
10
  __author__ = "klrc"
11
- __email__ = "144069824@qq.com"
11
+ __email__ = "1440698245@qq.com"
12
12
 
13
- __all__ = ["Demoboard"]
13
+ __all__ = ["SerialDevice"]
@@ -0,0 +1,81 @@
1
+ """
2
+ 命令行入口:sdev shell "cmd" 在串口设备上执行单条命令并回显输出。
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ import sys
8
+
9
+ from .core import SerialDevice
10
+
11
+
12
+ def _get_default_port() -> str:
13
+ return os.environ.get("SDEV_PORT", "/dev/ttyUSB0")
14
+
15
+
16
+ def _get_default_baudrate() -> int:
17
+ try:
18
+ return int(os.environ.get("SDEV_BAUDRATE", "115200"))
19
+ except ValueError:
20
+ return 115200
21
+
22
+
23
+ def cmd_shell(args: argparse.Namespace) -> int:
24
+ port = args.port or _get_default_port()
25
+ baudrate = args.baudrate or _get_default_baudrate()
26
+ with SerialDevice(port, baudrate, check_alive=not args.no_check_alive) as board:
27
+ try:
28
+ board.execute_command(args.command, flag=args.flag, timeout=args.timeout)
29
+ except TimeoutError as e:
30
+ print(e, file=sys.stderr)
31
+ return 1
32
+ return 0
33
+
34
+
35
+ def main() -> int:
36
+ parser = argparse.ArgumentParser(
37
+ prog="sdev",
38
+ description="串口开发板控制器:在串口设备上执行命令或等待提示符。",
39
+ )
40
+ parser.add_argument(
41
+ "-p", "--port",
42
+ default=None,
43
+ help="串口路径(默认: SDEV_PORT 或 /dev/ttyUSB0)",
44
+ )
45
+ parser.add_argument(
46
+ "-b", "--baudrate",
47
+ type=int,
48
+ default=None,
49
+ help="波特率(默认: SDEV_BAUDRATE 或 115200)",
50
+ )
51
+ subparsers = parser.add_subparsers(dest="subcommand", required=True)
52
+
53
+ shell_parser = subparsers.add_parser("shell", help="在板子上执行单条命令并回显输出")
54
+ shell_parser.add_argument(
55
+ "command",
56
+ help="要执行的命令,如: ls 或 \"lsmod | grep nnp\"",
57
+ )
58
+ shell_parser.add_argument(
59
+ "--flag",
60
+ default=SerialDevice.STABLE_FLAG,
61
+ help="提示符结束标志(默认: %(default)r)",
62
+ )
63
+ shell_parser.add_argument(
64
+ "--timeout",
65
+ type=float,
66
+ default=None,
67
+ help="等待命令输出的超时秒数",
68
+ )
69
+ shell_parser.add_argument(
70
+ "--no-check-alive",
71
+ action="store_true",
72
+ help="connect 后不执行 check_alive",
73
+ )
74
+ shell_parser.set_defaults(func=cmd_shell)
75
+
76
+ parsed = parser.parse_args()
77
+ return parsed.func(parsed)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ sys.exit(main())
@@ -0,0 +1,249 @@
1
+ """
2
+ 串口设备:连接、后台按行读、发送、按 flag 扫描/回显、执行命令、存活检测。
3
+ """
4
+
5
+ from itertools import chain
6
+ import queue
7
+ import threading
8
+ import time
9
+ from typing import Literal, Optional
10
+
11
+ from loguru import logger
12
+ import serial
13
+
14
+ CTRL_C = "\x03"
15
+
16
+
17
+ def _content_contains_ctrl_c(text: str) -> bool:
18
+ """是否包含设备回显的 Ctrl+C(\\x03 或 ^C、^C\\x00 等)。"""
19
+ n = text.replace("\x00", "")
20
+ return CTRL_C in n or "^C" in n
21
+
22
+
23
+ def _line_matches_flag(line: str, flag: str) -> bool:
24
+ """行(rstrip 后)是否以 flag 结尾;CTRL_C 兼容 \\x03 / \"^C\" 等回显形式。"""
25
+ stripped = line.rstrip()
26
+ if flag != CTRL_C:
27
+ return stripped.endswith(flag)
28
+ normalized = stripped.strip().replace("\x00", "")
29
+ return normalized == CTRL_C or normalized == "^C" or normalized.endswith("^C") or stripped.endswith(flag)
30
+
31
+
32
+ _GREY = "\033[90m"
33
+ _SENT = "\033[36m"
34
+ _GREEN = "\033[32m"
35
+ _RESET = "\033[0m"
36
+
37
+
38
+ class SerialDevice:
39
+ STABLE_FLAG = " #"
40
+
41
+ def __init__(self, port: str, baudrate: int = 115200, check_alive=True):
42
+ self.port = port
43
+ self.baudrate = baudrate
44
+ self._serial: Optional[serial.Serial] = None
45
+ self._buffer: queue.Queue = queue.Queue()
46
+ self._reader_thread: Optional[threading.Thread] = None
47
+ self._is_connected = False
48
+ self._stop_reading = False
49
+ self._display = True
50
+ self._check_alive = check_alive
51
+
52
+ self._reboot_timeout = 20 # check_alive 时 wait_for_flag 等稳定提示符的最大秒数
53
+ self._check_alive_timeout = 0.5
54
+ self._join_timeout = 2.0
55
+ self._scan_timeout = 0.1
56
+
57
+ def _serial_reader(self) -> None:
58
+ """后台 daemon:readline → UTF-8 解码 → put(line);解码失败则跳过该行。disconnect 时 _stop_reading + put(None) 唤醒阻塞 get()。"""
59
+ while not self._stop_reading and self._serial is not None and self._serial.is_open:
60
+ raw = self._serial.readline()
61
+ if raw:
62
+ try:
63
+ line = raw.decode("utf-8")
64
+ if line:
65
+ self._buffer.put(line)
66
+ except UnicodeDecodeError:
67
+ continue
68
+
69
+ def connect(self):
70
+ """打开串口、启动 _serial_reader;已连接则直接 return。若 check_alive 则 connect 后自动 check_alive(STABLE_FLAG)。"""
71
+ if self._is_connected:
72
+ return
73
+ self._serial = serial.Serial(self.port, self.baudrate, timeout=self._scan_timeout)
74
+ if not self._serial.is_open:
75
+ raise RuntimeError(f"无法打开串口 {self.port}")
76
+ self._stop_reading = False
77
+ self._reader_thread = threading.Thread(target=self._serial_reader, daemon=True)
78
+ self._reader_thread.start()
79
+ self._is_connected = True
80
+
81
+ self._max_lines_per_prompt = 2
82
+
83
+ if self._check_alive:
84
+ self.check_alive(self.STABLE_FLAG)
85
+
86
+ def disconnect(self):
87
+ """置 _stop_reading、put(None) 唤醒 get()、join 读线程、关串口;幂等。"""
88
+ if not self._is_connected:
89
+ return
90
+ self._stop_reading = True
91
+ self._buffer.put(None) # unblock any read() waiting on get()
92
+ if self._reader_thread is not None and self._reader_thread.is_alive():
93
+ self._reader_thread.join(timeout=self._join_timeout)
94
+ self._reader_thread = None
95
+ if self._serial is not None and self._serial.is_open:
96
+ self._serial.close()
97
+ self._serial = None
98
+ self._is_connected = False
99
+
100
+ def send(self, prompt: str):
101
+ """发送 prompt + 换行并 flush;prompt 不得含 \\n/\\r。"""
102
+ if not self._is_connected or self._serial is None:
103
+ raise RuntimeError("not connected")
104
+ assert "\n" not in prompt and "\r" not in prompt, "send(prompt) 不得包含换行符"
105
+ self._serial.write((prompt + "\n").encode("utf-8"))
106
+ self._serial.flush()
107
+
108
+ def _scan(self, timeout: Optional[float] = None):
109
+ """从 _buffer 逐行 yield;有 timeout 时每轮用 scan_timeout 轮询,总时长超时抛 TimeoutError;收到 None 结束。"""
110
+ if not self._is_connected:
111
+ raise RuntimeError("not connected")
112
+ deadline = (time.monotonic() + timeout) if timeout is not None else None
113
+ scan_timeout = self._scan_timeout if deadline is not None else None
114
+
115
+ while True: # timeout loop
116
+ if deadline is not None and time.monotonic() > deadline:
117
+ raise TimeoutError("scan deadline exceeded")
118
+ try:
119
+ line = self._buffer.get(timeout=scan_timeout) if scan_timeout else self._buffer.get()
120
+ except queue.Empty:
121
+ continue
122
+ if line is None: # _serial_reader sentinel
123
+ return
124
+ yield line
125
+
126
+ def _echo_prompt(self, line: str) -> None:
127
+ print(f"{_SENT}{line}{_RESET}", end="", flush=True)
128
+
129
+ def _echo_normal(self, line: str) -> None:
130
+ print(f"{_GREY}{line}{_RESET}", end="", flush=True)
131
+
132
+ def _echo_flag(self, line: str) -> None:
133
+ print(f"{_GREEN}{line}{_RESET}", end="", flush=True)
134
+
135
+ def scan(self, end_flag: str, timeout: Optional[float] = None, replace_with_acc=False):
136
+ """
137
+ 从 _buffer 逐行 yield,直到出现 end_flag 或超时/None。
138
+ 逻辑:用 acc_cache 保留最近几行,拼成 acc_line(无换行),避免设备折行把 flag 拆到两行导致 in 判不到;
139
+ 命中时若 replace_with_acc 则 yield 整段 acc_line 加换行,否则逐行 yield cache 后 break。CTRL_C 按 _content_contains_ctrl_c 判。
140
+ """
141
+ acc_cache = []
142
+ for line in self._scan(timeout):
143
+ acc_cache.append(line)
144
+ if len(acc_cache) > self._max_lines_per_prompt:
145
+ yield acc_cache.pop(0)
146
+ acc_line = "".join([x.rstrip("\r\n") for x in acc_cache])
147
+ found = end_flag in acc_line or (end_flag == CTRL_C and _content_contains_ctrl_c(acc_line))
148
+ if found:
149
+ if replace_with_acc:
150
+ yield acc_line + "\n"
151
+ else:
152
+ for x in acc_cache:
153
+ yield x
154
+ break
155
+
156
+ def scan_and_display(self, flag: str, flag_type: Literal["end_flag", "prompt"], timeout: Optional[float] = None, replace_with_acc=False):
157
+ """对 scan(flag) 的每一行按 flag_type 回显(end_flag 绿、prompt 青、其余灰)并 yield 该行;超时重抛。"""
158
+ try:
159
+ for line in self.scan(flag, timeout=timeout, replace_with_acc=replace_with_acc):
160
+ if self._display:
161
+ find_flag = _line_matches_flag(line, flag)
162
+ if find_flag:
163
+ if flag_type == "end_flag":
164
+ self._echo_flag(line)
165
+ elif flag_type == "prompt":
166
+ self._echo_prompt(line)
167
+ else:
168
+ self._echo_normal(line)
169
+ yield line
170
+ except TimeoutError:
171
+ t = f"{timeout}s" if timeout is not None else "None"
172
+ raise TimeoutError(f"scan_and_display timeout: flag={flag!r}, flag_type={flag_type!r}, timeout={t}")
173
+
174
+ def clear(self):
175
+ """清空 _buffer 中已入队行;取到 None 则 put(None) 后 return(保留 sentinel),用于 execute_command 前清残留。"""
176
+ while True:
177
+ try:
178
+ item = self._buffer.get_nowait()
179
+ if item is None:
180
+ self._buffer.put(None)
181
+ return
182
+ except queue.Empty:
183
+ return
184
+
185
+ def execute_command(
186
+ self,
187
+ cmd: str,
188
+ flag: str = " #",
189
+ stream: bool = False,
190
+ timeout: Optional[float] = None,
191
+ ):
192
+ """
193
+ 执行单条命令并收集到「含 flag 的结束行」为止的输出。
194
+ 逻辑:clear → send(cmd) → 两段 chain:先 scan(cmd) 跳过命令回显行,再 scan(flag) 收到提示符行;
195
+ cmd=CTRL_C 时第一段会按 ^C 回显形式识别。stream=False 返回 list,True 返回生成器;超时抛 TimeoutError。
196
+ """
197
+ self.clear()
198
+ self.send(cmd)
199
+ gen = chain(self.scan_and_display(cmd, "prompt", timeout=timeout, replace_with_acc=True), self.scan_and_display(flag, "end_flag", timeout=timeout))
200
+ if stream:
201
+ return gen
202
+ return list(gen)
203
+
204
+ def _has_output_in_buffer(self) -> bool:
205
+ """qsize() > 0 即视为有数据(多线程下为近似值)。"""
206
+ return self._buffer.qsize() > 0
207
+
208
+ def send_interrupt(self, timeout: float, end_flag: str = " #") -> None:
209
+ """发送 CTRL_C 后 execute_command(cmd=CTRL_C, flag=end_flag, timeout=timeout);超时抛 TimeoutError。"""
210
+ self.execute_command(cmd=CTRL_C, flag=end_flag, timeout=timeout)
211
+
212
+ def wait_for_flag(self, flag: str, timeout: float):
213
+ """消费 scan_and_display(flag, \"end_flag\", timeout),直到出现 flag 或超时。"""
214
+ gen = self.scan_and_display(flag, "end_flag", timeout=timeout)
215
+ _ = list(gen)
216
+
217
+ def check_alive(self, stable_flag: str):
218
+ """
219
+ 确认板子存活:有输出则 wait_for_flag(stable_flag) 后 return;否则 send_interrupt,成功则 return。
220
+ 若 send_interrupt 超时则打 warning 并轮询 _has_output_in_buffer(),有数据再 wait_for_flag 后 return;未接串口会一直循环。
221
+ """
222
+ time.sleep(self._check_alive_timeout)
223
+ if self._has_output_in_buffer(): # 可能正在启动,等待稳定标志
224
+ self.wait_for_flag(stable_flag, self._reboot_timeout)
225
+ return
226
+
227
+ # 测试是否能够接收ctrlc信号,若无反应, wait for boot
228
+ try:
229
+ self.send_interrupt(self._check_alive_timeout, stable_flag)
230
+ except TimeoutError:
231
+ logger.warning(f"请接入串口")
232
+ while True:
233
+ time.sleep(self._check_alive_timeout)
234
+ if self._has_output_in_buffer():
235
+ self.wait_for_flag(stable_flag, self._reboot_timeout)
236
+ return
237
+
238
+ def __enter__(self):
239
+ self.connect()
240
+ return self
241
+
242
+ def __exit__(self, exc_type, exc_value, traceback):
243
+ self.disconnect()
244
+ print()
245
+
246
+
247
+ if __name__ == "__main__":
248
+ with SerialDevice("/dev/ttyUSB0", 115200) as board:
249
+ board.execute_command("ls")
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: sdev
3
+ Version: 0.3.0
4
+ Summary: 串口控制器工具包
5
+ Home-page: https://github.com/klrc/sdev
6
+ Author: klrc
7
+ Author-email: klrc <144069824@qq.com>
8
+ License: MIT
9
+ Project-URL: Homepage, https://github.com/klrc/sdev
10
+ Project-URL: Repository, https://github.com/klrc/sdev
11
+ Project-URL: Bug Tracker, https://github.com/klrc/sdev/issues
12
+ Keywords: serial,controller,hardware,embedded
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.7
19
+ Classifier: Programming Language :: Python :: 3.8
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
+ Classifier: Topic :: System :: Hardware
25
+ Requires-Python: >=3.7
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Requires-Dist: pyserial>=3.5
29
+ Requires-Dist: loguru>=0.6.0
30
+ Dynamic: author
31
+ Dynamic: home-page
32
+ Dynamic: license-file
33
+ Dynamic: requires-python
34
+
35
+ # SDEV
36
+
37
+ 串口开发板控制器:后台按行读缓冲、按 flag 扫描与回显着色、执行命令、存活检测。
38
+
39
+ ## 安装
40
+
41
+ ```bash
42
+ pip install sdev
43
+ ```
44
+
45
+ ## 命令行
46
+
47
+ 安装后可使用 `sdev` 命令,在默认串口上执行单条命令并回显:
48
+
49
+ ```bash
50
+ sdev shell "lsmod | grep -E 'mmz|nnp|vdsp'"
51
+ sdev -p /dev/ttyUSB1 -b 115200 shell "ls"
52
+ sdev --no-check-alive shell "echo hello"
53
+ ```
54
+
55
+ - `-p` / `--port`:串口路径(默认取环境变量 `SDEV_PORT` 或 `/dev/ttyUSB0`)
56
+ - `-b` / `--baudrate`:波特率(默认取 `SDEV_BAUDRATE` 或 `115200`)
57
+ - `--flag`:提示符结束标志(默认 `" #"`)
58
+ - `--timeout`:等待输出超时秒数
59
+ - `--no-check-alive`:连接后不执行存活检测
60
+
61
+ ## 快速使用(Python)
62
+
63
+ ```python
64
+ from sdev import SerialDevice
65
+
66
+ with SerialDevice("/dev/ttyUSB0", 115200) as board:
67
+ # connect 时默认会 check_alive(STABLE_FLAG),未接串口会轮询等待
68
+ out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
69
+ print(out)
70
+ ```
71
+
72
+ 不自动做存活检测时:
73
+
74
+ ```python
75
+ board = SerialDevice("/dev/ttyUSB0", 115200, check_alive=False)
76
+ board.connect()
77
+ board.wait_for_flag("~ #", timeout=20) # 可选:等到指定提示符
78
+ board.execute_command("ls")
79
+ board.disconnect()
80
+ ```
81
+
82
+ ## 主要接口
83
+
84
+ | 方法 / 属性 | 说明 |
85
+ |-------------|------|
86
+ | `SerialDevice.STABLE_FLAG` | 类常量 `" #"`,常用作稳定提示符 |
87
+ | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程;`connect` 可选执行 `check_alive(STABLE_FLAG)` |
88
+ | `send(text)` | 发送一行(自动加换行);`text` 不得含 `\n` / `\r` |
89
+ | `execute_command(cmd, flag=" #", stream=False, timeout=None)` | 清缓冲 → 发 `cmd` → 先等命令回显再等含 `flag` 的提示行;返回列表或生成器;`cmd=Ctrl+C` 时兼容 ^C 回显 |
90
+ | `check_alive(stable_flag)` | 有输出则 `wait_for_flag(stable_flag)` 后返回;否则发 Ctrl-C 再等;超时则打 warning 并轮询直到有输出 |
91
+ | `wait_for_flag(flag, timeout)` | 消费输出直到出现 `flag` 或超时 |
92
+ | `send_interrupt(timeout, end_flag=" #")` | 发 Ctrl-C 并等含 `end_flag` 的提示行;超时抛 `TimeoutError` |
93
+ | `clear()` | 清空读缓冲(保留 sentinel),一般由 `execute_command` 内部调用 |
94
+
95
+ - 回显:匹配到 flag 的行为绿色,命令回显行为青色,其余灰色;设备折行会拼行再判 flag。
96
+
97
+ ## 依赖
98
+
99
+ - Python >= 3.7
100
+ - pyserial >= 3.5
101
+ - loguru >= 0.6.0
@@ -4,9 +4,11 @@ README.md
4
4
  pyproject.toml
5
5
  setup.py
6
6
  sdev/__init__.py
7
- sdev/demoboard.py
7
+ sdev/cli_wrapper.py
8
+ sdev/core.py
8
9
  sdev.egg-info/PKG-INFO
9
10
  sdev.egg-info/SOURCES.txt
10
11
  sdev.egg-info/dependency_links.txt
12
+ sdev.egg-info/entry_points.txt
11
13
  sdev.egg-info/requires.txt
12
14
  sdev.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ sdev = sdev.cli_wrapper:main
@@ -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.2.2",
8
+ version="0.3.0",
9
9
  author="klrc",
10
10
  author_email="144069824@qq.com",
11
11
  description="串口控制器工具包",
@@ -30,5 +30,9 @@ setup(
30
30
  python_requires=">=3.7",
31
31
  install_requires=[
32
32
  "pyserial>=3.5",
33
+ "loguru>=0.6.0",
33
34
  ],
35
+ entry_points={
36
+ "console_scripts": ["sdev=sdev.cli_wrapper:main"],
37
+ },
34
38
  )
sdev-0.2.2/PKG-INFO DELETED
@@ -1,77 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sdev
3
- Version: 0.2.2
4
- Summary: 串口控制器工具包
5
- Home-page: https://github.com/klrc/sdev
6
- Author: klrc
7
- Author-email: klrc <144069824@qq.com>
8
- License: MIT
9
- Project-URL: Homepage, https://github.com/klrc/sdev
10
- Project-URL: Repository, https://github.com/klrc/sdev
11
- Project-URL: Bug Tracker, https://github.com/klrc/sdev/issues
12
- Keywords: serial,controller,hardware,embedded
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: MIT License
16
- Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
- Classifier: Programming Language :: Python :: 3.10
22
- Classifier: Programming Language :: Python :: 3.11
23
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Classifier: Topic :: System :: Hardware
25
- Requires-Python: >=3.7
26
- Description-Content-Type: text/markdown
27
- License-File: LICENSE
28
- Requires-Dist: pyserial>=3.5
29
- Requires-Dist: loguru>=0.6.0
30
- Dynamic: author
31
- Dynamic: home-page
32
- Dynamic: license-file
33
- Dynamic: requires-python
34
-
35
- # SDEV
36
-
37
- 串口开发板控制器工具包:后台异步读缓冲、按行/按 flag 读、回显着色、组合命令、存活检测。
38
-
39
- ## 安装
40
-
41
- ```bash
42
- pip install sdev
43
- ```
44
-
45
- ## 快速使用
46
-
47
- ```python
48
- from sdev import Demoboard
49
-
50
- with Demoboard("/dev/ttyUSB0", 115200) as board:
51
- board.check_alive("~ #") # 确认板子存活(未接则轮询等待)
52
- out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
53
- print(out)
54
- ```
55
-
56
- ## 主要接口
57
-
58
- | 方法 | 说明 |
59
- |------|------|
60
- | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程 |
61
- | `send(text)` | 发送一行(自动加换行);`text` 不得含换行符 |
62
- | `read(display=..., timeout=...)` | 消费缓冲直至断开或超时,返回行列表 |
63
- | `read_until(flag, timeout=..., skip_echo_of=...)` | 读到含 `flag` 且非「命令回显」的行为止 |
64
- | `execute_command(cmd, flag=" #", stream=False, ...)` | 先 `clear()` 再 `send(cmd)` 再 `read_until(flag, skip_echo_of=cmd)` |
65
- | `check_alive(reboot_flag=..., prompt_flag=" #", ...)` | 有缓冲数据或能响应 Ctrl-C 即视为存活;超时则轮询等待 |
66
- | `clear()` | 清空读缓冲(保留 disconnect 用 sentinel) |
67
- | `send_interrupt(prompt_flag=..., timeout=...)` | 发 Ctrl-C 并等到含 `prompt_flag` 的提示行 |
68
-
69
- - 使用 `stream_read` / `stream_read_until` 可逐行迭代;`read` / `read_until` 为列表版。
70
- - `display=True` 时:本机发送回显为青色,匹配 flag 行为绿色,其余为灰色;板端折行会拼成一行再显示。
71
-
72
- ## 特性
73
-
74
- - 后台线程读串口入队,主流程不阻塞
75
- - 不假定提示符左边格式(如 `~ #` / ` #` 均可),通过 `skip_echo_of` 区分命令回显与提示行
76
- - 超时在底层 `get(timeout=...)` 实现,可靠
77
- - 依赖:Python >= 3.7,pyserial >= 3.5,loguru >= 0.6.0
sdev-0.2.2/README.md DELETED
@@ -1,43 +0,0 @@
1
- # SDEV
2
-
3
- 串口开发板控制器工具包:后台异步读缓冲、按行/按 flag 读、回显着色、组合命令、存活检测。
4
-
5
- ## 安装
6
-
7
- ```bash
8
- pip install sdev
9
- ```
10
-
11
- ## 快速使用
12
-
13
- ```python
14
- from sdev import Demoboard
15
-
16
- with Demoboard("/dev/ttyUSB0", 115200) as board:
17
- board.check_alive("~ #") # 确认板子存活(未接则轮询等待)
18
- out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
19
- print(out)
20
- ```
21
-
22
- ## 主要接口
23
-
24
- | 方法 | 说明 |
25
- |------|------|
26
- | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程 |
27
- | `send(text)` | 发送一行(自动加换行);`text` 不得含换行符 |
28
- | `read(display=..., timeout=...)` | 消费缓冲直至断开或超时,返回行列表 |
29
- | `read_until(flag, timeout=..., skip_echo_of=...)` | 读到含 `flag` 且非「命令回显」的行为止 |
30
- | `execute_command(cmd, flag=" #", stream=False, ...)` | 先 `clear()` 再 `send(cmd)` 再 `read_until(flag, skip_echo_of=cmd)` |
31
- | `check_alive(reboot_flag=..., prompt_flag=" #", ...)` | 有缓冲数据或能响应 Ctrl-C 即视为存活;超时则轮询等待 |
32
- | `clear()` | 清空读缓冲(保留 disconnect 用 sentinel) |
33
- | `send_interrupt(prompt_flag=..., timeout=...)` | 发 Ctrl-C 并等到含 `prompt_flag` 的提示行 |
34
-
35
- - 使用 `stream_read` / `stream_read_until` 可逐行迭代;`read` / `read_until` 为列表版。
36
- - `display=True` 时:本机发送回显为青色,匹配 flag 行为绿色,其余为灰色;板端折行会拼成一行再显示。
37
-
38
- ## 特性
39
-
40
- - 后台线程读串口入队,主流程不阻塞
41
- - 不假定提示符左边格式(如 `~ #` / ` #` 均可),通过 `skip_echo_of` 区分命令回显与提示行
42
- - 超时在底层 `get(timeout=...)` 实现,可靠
43
- - 依赖:Python >= 3.7,pyserial >= 3.5,loguru >= 0.6.0
@@ -1,382 +0,0 @@
1
- """
2
- Demoboard: 串口连接、后台异步读、发送、按行/按 flag 读、回显、组合命令、存活检测。
3
- 不兼容旧 SerialController/Demoboard 实现。
4
- """
5
-
6
- import queue
7
- import sys
8
- import threading
9
- import time
10
- from typing import Generator, List, Optional, Union
11
-
12
- from loguru import logger
13
- import serial
14
-
15
- CTRL_C = "\x03"
16
-
17
- # ANSI(发送回显用青色,比紫色柔和、与绿色区分明显)
18
- _GREY = "\033[90m"
19
- _SENT = "\033[36m" # cyan 青色,护眼
20
- _GREEN = "\033[32m"
21
- _RESET = "\033[0m"
22
-
23
-
24
- def _echo_grey(line: str) -> None:
25
- """普通设备输出:整行灰色打印。flush 保证与串口交错时顺序正确。"""
26
- print(f"{_GREY}{line}{_RESET}", end="", flush=True)
27
-
28
-
29
- def _echo_sent(line: str) -> None:
30
- """本机发送内容的回显:整行青色(含 CTRL_C 显示为 ^C 的行)。"""
31
- print(f"{_SENT}{line}{_RESET}", end="", flush=True)
32
-
33
-
34
- def _echo_green(line: str) -> None:
35
- """read_until 匹配到 flag 的那一行:整行绿色(如提示符行)。"""
36
- print(f"{_GREEN}{line}{_RESET}", end="", flush=True)
37
-
38
-
39
- class Demoboard:
40
- """
41
- 串口开发板控制器:连接、后台异步读缓冲、发送、按行/按 flag 读、回显、组合命令、存活检测。
42
-
43
- 构造后需显式 connect(),或使用 with Demoboard(port) as board。
44
- """
45
-
46
- def __init__(self, port: str, baudrate: int = 115200) -> None:
47
- """
48
- 只保存端口和波特率,不打开串口、不启动读线程。
49
-
50
- - 连接需显式调用 connect() 或使用 with。
51
- - 未 connect 前调用 send/read 会抛 RuntimeError。
52
- """
53
- self.port = port
54
- self.baudrate = baudrate
55
- self._serial: Optional[serial.Serial] = None
56
- self._buffer: queue.Queue = queue.Queue()
57
- self._reader_thread: Optional[threading.Thread] = None
58
- self._stop_reading = False
59
- self._connected = False
60
- self._pending_sent: Optional[str] = None # 仅作标记,在 read 中匹配到时用 _echo_sent 打印一次
61
- self._pending_flag: Optional[str] = None # 仅作标记,在 read 中匹配到时整行绿色打印一次
62
-
63
- def _serial_reader(self) -> None:
64
- """
65
- 后台线程:从串口按行读入并放入 _buffer。
66
-
67
- 行为:
68
- - 循环 readline(),UTF-8 解码并 strip,非空则 put(line)。
69
- - 解码失败则跳过该行;OSError/SerialException 则退出循环。
70
-
71
- 原理:及时消费串口,避免缓冲满丢数,且不阻塞主流程。
72
- 注意:daemon 线程;disconnect 时通过 _stop_reading 与 put(None) 结束并唤醒阻塞的 get()。
73
- """
74
- while not self._stop_reading and self._serial is not None and self._serial.is_open:
75
- try:
76
- raw = self._serial.readline()
77
- if raw:
78
- try:
79
- line = raw.decode("utf-8")
80
- if line:
81
- self._buffer.put(line)
82
- except UnicodeDecodeError:
83
- continue
84
- except (OSError, serial.SerialException):
85
- break
86
- except Exception:
87
- continue
88
-
89
- def _has_output_in_buffer(self) -> bool:
90
- """
91
- 队列里是否已有数据(qsize > 0 即视为有)。
92
-
93
- 用于 check_alive:有任意输出就认为设备存活,不依赖提示符格式。
94
- 注意:多线程下 qsize() 为近似值,用于布尔判断足够。
95
- """
96
- return self._buffer.qsize() > 0
97
-
98
- def _is_echo_of(self, line: str, sent: str) -> bool:
99
- """
100
- 判断该行是否为「刚发送内容」的回显。
101
-
102
- 行为:
103
- - line 等于 sent,或以 " " + sent 结尾 → 视为回显。
104
- - sent 为 CTRL_C 时,设备常显示 "^C",也视为回显。
105
-
106
- 用于 read_until(..., skip_echo_of=...):区分命令回显行(如 "~ # ls")和纯提示符行(如 "~ #"),
107
- 不假定提示符左边格式。仅做字符串匹配,sent 为普通命令或 CTRL_C 即可。
108
- """
109
- s = line.rstrip()
110
- if s == sent or s.endswith(" " + sent):
111
- return True
112
- if sent == CTRL_C and (s.strip() == "^C" or "^C" in line or CTRL_C in line):
113
- return True
114
- return False
115
-
116
- @property
117
- def is_connected(self) -> bool:
118
- """是否已连接(串口已打开且读线程已启动)。"""
119
- return self._connected
120
-
121
- def connect(self) -> None:
122
- """
123
- 打开串口并启动后台读线程,后续数据写入 _buffer。
124
-
125
- 行为:创建 Serial(port, baudrate, timeout=0.1),启动 _serial_reader,置 _connected=True。
126
- 已连接则直接 return(幂等)。
127
- 注意:串口打开失败只打 error 日志并 return,不抛异常;调用方用 is_connected 判断。
128
- """
129
- if self._connected:
130
- return
131
- self._serial = serial.Serial(self.port, self.baudrate, timeout=0.1)
132
- if not self._serial.is_open:
133
- logger.error(f"无法打开串口 {self.port}")
134
- return
135
- self._stop_reading = False
136
- self._reader_thread = threading.Thread(target=self._serial_reader, daemon=True)
137
- self._reader_thread.start()
138
- self._connected = True
139
-
140
- def disconnect(self) -> None:
141
- """
142
- 停止读线程并关闭串口,幂等。
143
-
144
- 行为:置 _stop_reading,put(None) 唤醒阻塞的 get(),join 读线程,关闭串口。
145
- None 为 sentinel,stream_read 收到后结束迭代。未连接则直接 return。
146
- 注意:join 最多等 2s,超时后仍会关串口,不抛异常。
147
- """
148
- if not self._connected:
149
- return
150
- self._stop_reading = True
151
- self._buffer.put(None) # unblock any read() waiting on get()
152
- if self._reader_thread is not None and self._reader_thread.is_alive():
153
- self._reader_thread.join(timeout=2.0)
154
- self._reader_thread = None
155
- if self._serial is not None and self._serial.is_open:
156
- self._serial.close()
157
- self._serial = None
158
- self._connected = False
159
-
160
- def send(self, text: str) -> None:
161
- """
162
- 向串口发送 text + 换行,不等待回显。
163
-
164
- 行为:写入 (text + "\\n") 并 flush,同时设 _pending_sent = text。
165
- 回显在 stream_read 里在匹配到 sent 前扣押多行、去换行拼接,匹配后整行青色打印。
166
- 未连接抛 RuntimeError;text 不得含换行符(便于与板端折行回显拼接匹配)。
167
- """
168
- if not self._connected or self._serial is None:
169
- raise RuntimeError("not connected")
170
- assert "\n" not in text and "\r" not in text, "send(text) 不得包含换行符"
171
- self._serial.write((text + "\n").encode("utf-8"))
172
- self._serial.flush()
173
- self._pending_sent = text
174
-
175
- def _sent_echo_matches(self, acc: str, sent: str) -> bool:
176
- """拼接后的 acc 行尾是否匹配本次发送内容(用于板端折行回显)。"""
177
- if not acc:
178
- return False
179
- s = acc.rstrip()
180
- if s == sent or s.endswith(" " + sent):
181
- return True
182
- if sent == CTRL_C and (s.strip() == "^C" or "^C" in acc or CTRL_C in acc):
183
- return True
184
- return False
185
-
186
- def stream_read(self, display: bool = True, timeout: Optional[float] = None) -> Generator[str, None, None]:
187
- """
188
- 从 _buffer 逐行 yield,直到 disconnect 或超时。
189
-
190
- 行为:
191
- - 收到 None 则结束迭代。
192
- - display 且 _pending_sent 不为 None 时:持续 get 并扣押,去换行拼接为 acc,直到 acc 行尾匹配 sent,再整行青色打印并 yield 已扣押的每一行;保证 send 的输入完整显示为一行(板端折行也会被拼回)。
193
- - 其余行:按 flag/sent 匹配做绿/青/灰单行显示。
194
- """
195
- if not self._connected:
196
- raise RuntimeError("not connected")
197
- deadline = (time.monotonic() + timeout) if timeout is not None else None
198
- get_timeout = 0.1 if deadline is not None else None
199
- while True:
200
- if deadline is not None and time.monotonic() > deadline:
201
- raise TimeoutError("read deadline exceeded")
202
- if display and self._pending_sent is not None:
203
- acc = ""
204
- lines_buf: List[str] = []
205
- while True:
206
- try:
207
- line = self._buffer.get(timeout=get_timeout) if get_timeout else self._buffer.get()
208
- except queue.Empty:
209
- continue
210
- if line is None:
211
- if lines_buf:
212
- _echo_grey(acc + "\n")
213
- for L in lines_buf:
214
- yield L
215
- self._buffer.put(None)
216
- return
217
- lines_buf.append(line)
218
- acc += line.rstrip("\r\n")
219
- if self._sent_echo_matches(acc, self._pending_sent):
220
- self._pending_sent = None
221
- _echo_sent(acc + "\n")
222
- for L in lines_buf:
223
- yield L
224
- break
225
- continue
226
- try:
227
- line = self._buffer.get(timeout=get_timeout) if get_timeout else self._buffer.get()
228
- except queue.Empty:
229
- continue
230
- if line is None:
231
- return
232
- if display:
233
- flag_ok = self._pending_flag is not None and self._pending_flag in line
234
- if flag_ok:
235
- self._pending_flag = None
236
- _echo_green(line)
237
- else:
238
- _echo_grey(line)
239
- yield line
240
-
241
- def stream_read_until(
242
- self,
243
- flag: str,
244
- timeout: Optional[float] = None,
245
- display: bool = True,
246
- skip_echo_of: Optional[str] = None,
247
- ) -> Generator[str, None, None]:
248
- """
249
- 从 _buffer 逐行 yield,直到某行「包含 flag 且不是 skip_echo_of 的回显」则结束(该行会 yield)。
250
-
251
- 行为:
252
- - 设 _pending_flag=flag,迭代 stream_read。
253
- - 若行含 flag:当 skip_echo_of 不为 None 且该行是「刚发送内容的回显」则继续读(跳过 "~ # ls" 这类);否则 return。
254
-
255
- 与旧 SerialController 一致,不假定提示符左边格式;flag 如 " #"、"~ #" 通用。
256
- skip_echo_of=None 时等价于「包含 flag 即停」。超时抛带 flag/timeout 信息的 TimeoutError。
257
- """
258
- self._pending_flag = flag
259
- try:
260
- for line in self.stream_read(display=display, timeout=timeout):
261
- yield line
262
- if flag not in line:
263
- continue
264
- if skip_echo_of is not None and self._is_echo_of(line, skip_echo_of):
265
- continue
266
- return
267
- except TimeoutError:
268
- raise TimeoutError(f"read_until timeout: flag={flag!r}, timeout={timeout}s") from None
269
- finally:
270
- self._pending_flag = None
271
-
272
- def read(self, display: bool = True, timeout: Optional[float] = None) -> List[str]:
273
- """把 stream_read 全部消费成列表返回。超时抛 TimeoutError。"""
274
- return list(self.stream_read(display, timeout))
275
-
276
- def read_until(
277
- self,
278
- flag: str,
279
- timeout: Optional[float] = None,
280
- display: bool = True,
281
- skip_echo_of: Optional[str] = None,
282
- ) -> List[str]:
283
- """把 stream_read_until 全部消费成列表返回(含结束行)。超时抛 TimeoutError。"""
284
- return list(self.stream_read_until(flag, timeout, display, skip_echo_of))
285
-
286
- def clear(self) -> None:
287
- """
288
- 清空 _buffer 里已入队的行,不丢弃 sentinel(None)。
289
-
290
- 行为:循环 get_nowait(),取到 None 则 put(None) 后 return,否则丢弃;队列空则 return。
291
- 用于 execute_command 前清残留(与旧 SerialController 的 clear_queue 一致),避免上次提示符干扰本次匹配。
292
- 注意:取到 None 必须放回,否则 stream_read 无法正常结束。
293
- """
294
- while True:
295
- try:
296
- item = self._buffer.get_nowait()
297
- if item is None:
298
- self._buffer.put(None)
299
- return
300
- except queue.Empty:
301
- return
302
-
303
- def send_interrupt(self, prompt_flag: str = " #", timeout: float = 0.5) -> None:
304
- """
305
- 发送 CTRL_C,并等待出现含 prompt_flag 的提示行(跳过 ^C 回显行)。
306
-
307
- 内部:send(CTRL_C) 后 read_until(prompt_flag, timeout, display=True, skip_echo_of=CTRL_C)。
308
- 超时抛 TimeoutError,由 check_alive 等调用方处理。
309
- """
310
- self.send(CTRL_C)
311
- self.read_until(prompt_flag, timeout=timeout, display=True, skip_echo_of=CTRL_C)
312
-
313
- def check_alive(
314
- self,
315
- reboot_flag: Optional[str] = None,
316
- prompt_flag: str = " #",
317
- timeout: float = 0.5,
318
- ) -> None:
319
- """
320
- 确认开发板存活;能正常返回即表示 alive。
321
- 内容:若 _has_output_in_buffer() 为真则视为已有输出(如已进 shell),可选 read_until(reboot_flag) 吃到启动信息后 return;否则 send_interrupt(prompt_flag, timeout),成功则 return。若 send_interrupt 超时则打 warning,然后死循环:每隔 timeout 秒检查 _has_output_in_buffer(),有则可选 read_until(reboot_flag) 后 return。
322
- 原理:不依赖「正在输出」,只依赖「队列里曾有过数据」或「能响应 Ctrl-C 并回到 prompt」;重启上电后无输出则等 prompt,未接串口则一直轮询直到有输出。
323
- 边界:reboot_flag 用于上电后等待特定行(如 "~ #");未接串口时会永久循环并周期性 warning。
324
- """
325
- # wait for boot
326
- time.sleep(timeout)
327
- if self._has_output_in_buffer():
328
- if reboot_flag is not None:
329
- self.read_until(reboot_flag, display=True)
330
- return
331
-
332
- # 测试是否能够接收ctrlc信号,若无反应, wait for boot
333
- try:
334
- self.send_interrupt(prompt_flag, timeout)
335
- except TimeoutError:
336
- logger.warning(f"请接入串口")
337
- while True:
338
- time.sleep(timeout)
339
- if self._has_output_in_buffer():
340
- if reboot_flag is not None:
341
- self.read_until(reboot_flag, display=True)
342
- return
343
-
344
- def execute_command(
345
- self,
346
- cmd: str,
347
- flag: str = " #",
348
- stream: bool = False,
349
- timeout: Optional[float] = None,
350
- display: bool = True,
351
- ) -> Union[List[str], Generator[str, None, None]]:
352
- """
353
- 执行单条命令,并收集到「含 flag 的结束行」为止的输出。
354
-
355
- 行为:先 clear(),再 send(cmd),再 stream_read_until(flag, ..., skip_echo_of=cmd)。
356
- stream=False 返回整段输出的列表;stream=True 返回生成器。
357
-
358
- 与旧 SerialController 一致:clear 避免残留;skip_echo_of=cmd 让命令回显行(如 "~ # ls")不当作结束,
359
- 只在真正的新提示符行结束,不假定提示符左边格式。超时抛 TimeoutError;cmd 为 CTRL_C 时也会跳过 "^C" 行。
360
- """
361
- self.clear()
362
- self.send(cmd)
363
- gen = self.stream_read_until(flag, timeout=timeout, display=display, skip_echo_of=cmd)
364
- if stream:
365
- return gen
366
- return list(gen)
367
-
368
- def __enter__(self) -> "Demoboard":
369
- """with 入口:connect() 并返回 self。"""
370
- self.connect()
371
- return self
372
-
373
- def __exit__(self, exc_type: object, exc_val: object, exc_tb: object) -> None:
374
- """with 出口:disconnect();异常不吞,继续向外抛。"""
375
- self.disconnect()
376
- print() # 强制换行以刷新可能的日志末尾
377
-
378
-
379
- if __name__ == "__main__":
380
- with Demoboard("/dev/ttyUSB0", 115200) as board:
381
- board.check_alive("~ #")
382
- board.execute_command("ls")
@@ -1,77 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: sdev
3
- Version: 0.2.2
4
- Summary: 串口控制器工具包
5
- Home-page: https://github.com/klrc/sdev
6
- Author: klrc
7
- Author-email: klrc <144069824@qq.com>
8
- License: MIT
9
- Project-URL: Homepage, https://github.com/klrc/sdev
10
- Project-URL: Repository, https://github.com/klrc/sdev
11
- Project-URL: Bug Tracker, https://github.com/klrc/sdev/issues
12
- Keywords: serial,controller,hardware,embedded
13
- Classifier: Development Status :: 3 - Alpha
14
- Classifier: Intended Audience :: Developers
15
- Classifier: License :: OSI Approved :: MIT License
16
- Classifier: Operating System :: OS Independent
17
- Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.7
19
- Classifier: Programming Language :: Python :: 3.8
20
- Classifier: Programming Language :: Python :: 3.9
21
- Classifier: Programming Language :: Python :: 3.10
22
- Classifier: Programming Language :: Python :: 3.11
23
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
24
- Classifier: Topic :: System :: Hardware
25
- Requires-Python: >=3.7
26
- Description-Content-Type: text/markdown
27
- License-File: LICENSE
28
- Requires-Dist: pyserial>=3.5
29
- Requires-Dist: loguru>=0.6.0
30
- Dynamic: author
31
- Dynamic: home-page
32
- Dynamic: license-file
33
- Dynamic: requires-python
34
-
35
- # SDEV
36
-
37
- 串口开发板控制器工具包:后台异步读缓冲、按行/按 flag 读、回显着色、组合命令、存活检测。
38
-
39
- ## 安装
40
-
41
- ```bash
42
- pip install sdev
43
- ```
44
-
45
- ## 快速使用
46
-
47
- ```python
48
- from sdev import Demoboard
49
-
50
- with Demoboard("/dev/ttyUSB0", 115200) as board:
51
- board.check_alive("~ #") # 确认板子存活(未接则轮询等待)
52
- out = board.execute_command("ls") # 发命令并收到提示符为止,返回输出列表
53
- print(out)
54
- ```
55
-
56
- ## 主要接口
57
-
58
- | 方法 | 说明 |
59
- |------|------|
60
- | `connect()` / `disconnect()` | 打开/关闭串口并启停后台读线程 |
61
- | `send(text)` | 发送一行(自动加换行);`text` 不得含换行符 |
62
- | `read(display=..., timeout=...)` | 消费缓冲直至断开或超时,返回行列表 |
63
- | `read_until(flag, timeout=..., skip_echo_of=...)` | 读到含 `flag` 且非「命令回显」的行为止 |
64
- | `execute_command(cmd, flag=" #", stream=False, ...)` | 先 `clear()` 再 `send(cmd)` 再 `read_until(flag, skip_echo_of=cmd)` |
65
- | `check_alive(reboot_flag=..., prompt_flag=" #", ...)` | 有缓冲数据或能响应 Ctrl-C 即视为存活;超时则轮询等待 |
66
- | `clear()` | 清空读缓冲(保留 disconnect 用 sentinel) |
67
- | `send_interrupt(prompt_flag=..., timeout=...)` | 发 Ctrl-C 并等到含 `prompt_flag` 的提示行 |
68
-
69
- - 使用 `stream_read` / `stream_read_until` 可逐行迭代;`read` / `read_until` 为列表版。
70
- - `display=True` 时:本机发送回显为青色,匹配 flag 行为绿色,其余为灰色;板端折行会拼成一行再显示。
71
-
72
- ## 特性
73
-
74
- - 后台线程读串口入队,主流程不阻塞
75
- - 不假定提示符左边格式(如 `~ #` / ` #` 均可),通过 `skip_echo_of` 区分命令回显与提示行
76
- - 超时在底层 `get(timeout=...)` 实现,可靠
77
- - 依赖:Python >= 3.7,pyserial >= 3.5,loguru >= 0.6.0
File without changes
File without changes
File without changes
File without changes
File without changes