sdev 0.7.2__tar.gz → 0.7.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdev
3
- Version: 0.7.2
3
+ Version: 0.7.5
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -57,34 +57,41 @@ pip install sdev
57
57
 
58
58
  ## 命令行快速上手
59
59
 
60
- 安装后会提供一个 `sdev` 命令,包括:
61
-
62
- - `sdev list`:扫描本地 / 远程可用串口;
63
- - `sdev run`:在选定设备上执行命令;
64
- - `sdev server`:启动/停止本机串口共享服务;
60
+ 安装后提供一个 `sdev` 命令,核心功能包括:
61
+
62
+ - **`sdev list [scope] [filters...]`**:扫描本地 / 远程可用串口。
63
+ - `scope`: `local`, `remote` 或不填。
64
+ - `filters`: 如 `type=XC01` 或 `host=192.168.1.101`。
65
+ - `-f, --fast`: 快速模式,找到第一个符合条件的设备后立即输出(适合脚本)。
66
+ - **`sdev run [cmd...]`**:执行命令。
67
+ - `-i, --interrupt-only`: 仅发送一次 Ctrl-C,随后被动收集输出(不发送新命令)。
68
+ - `-t, --timeout`: 整体超时限制。
69
+ - `-w, --wait-forever`: 永久等待输出,直至整体超时或手动中断。
70
+ - **`sdev server [action]`**:管理本机串口共享服务。
71
+ - `action`: `start`, `stop`, `restart`, `status`。
72
+ - **`sdev mcp [action]`**:AI 集成(Model Context Protocol)。
73
+ - `action`: `install` (一键配置到 IDE), `run` (由 IDE 调起的后台服务)。
65
74
  - `sdev set` / `sdev show` / `sdev reset`:管理默认设备与缓存。
66
75
 
67
76
  典型流程:
68
77
 
69
78
  ```bash
70
- # 1. (可选)在“串口所在机器”上启动 server
79
+ # 1. 在“串口所在机器”上启动 server
71
80
  sdev server start
72
81
 
73
- # 2. 扫描可用设备
74
- sdev list
82
+ # 2. 扫描并筛选可用设备
83
+ sdev list remote # 仅扫描远程
84
+ sdev list type=XC01 # 仅列出特定型号
85
+ sdev list -f type=XC01 # 快速模式:找到第一个 XC01 立即返回(适合脚本)
75
86
 
76
- # 3. (推荐)选中一块板,配置为默认设备
87
+ # 3. 选中一块板,配置为默认设备
77
88
  sdev set default <DEVICE_ID>
78
89
 
79
- # 4. 之后即可直接执行命令
80
- sdev run 'cat /proc/meminfo'
81
- sdev run -t 10 'dmesg | tail'
90
+ # 4. 执行命令
91
+ sdev run 'cat /proc/meminfo' # 执行完整交互
92
+ sdev run -i # 仅发送 Ctrl-C (用于中断正在跑的任务并查看输出)
82
93
  ```
83
94
 
84
- 更详细的 CLI 用法与行为说明,参考项目内的 skill 文档:
85
-
86
- - `docs/skills/serial-sdev-cli/SKILL.md`
87
-
88
95
  ---
89
96
 
90
97
  ## Python API 快速上手(SerialNotebook)
@@ -125,6 +132,25 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
125
132
 
126
133
  ---
127
134
 
135
+ ## AI 集成 (MCP)
136
+
137
+ `sdev` 支持 **Model Context Protocol (MCP)**,允许 AI 助手(如 Cursor、Claude Desktop)直接操作串口,执行命令并感知设备状态。
138
+
139
+ ### 1. 获取配置信息
140
+ 在本地环境执行:
141
+ ```bash
142
+ sdev mcp install
143
+ ```
144
+ 该命令会自动生成可用于粘贴的 JSON 配置片段,并提示 Cursor 或 Claude Desktop 的配置路径。
145
+
146
+ ### 2. 提供的能力 (Tools)
147
+ 安装后,AI 助手将获得以下工具:
148
+ - `sdev_list`: 获取本地及远程可用设备列表。
149
+ - `sdev_run`: 在指定设备上执行命令并获取输出。
150
+ - `sdev_set_default`: 设置当前会话的默认设备。
151
+
152
+ ---
153
+
128
154
  ## 适用 / 不适用的场景
129
155
 
130
156
  **适用:**
@@ -143,9 +169,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
143
169
  ## 更多文档
144
170
 
145
171
  - **内部架构与设计说明**:`docs/arch_design.md`
146
- - **技能(在 Cursor 里集成使用方式)**:
147
- - `docs/skills/serial-sdev-cli/SKILL.md`
148
- - `docs/skills/serial-sdev-api/SKILL.md`
149
172
 
150
173
  源码结构概览(仅列出现行实现):
151
174
 
@@ -153,5 +176,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
153
176
  - `serial_rw.py`:本地串口读写与锁管理(`SerialRW`)、运行控制器(`SerialRunner`)、端口扫描与快速探测;
154
177
  - `serial_rw_server.py`:远程串口共享服务与客户端(`SerialRWServer` / `SerialRWClient`),以及主机发现逻辑;
155
178
  - `serial_notebook.py`:Notebook / 脚本场景下的最高层 Python 入口(`SerialNotebook`),基于 `SerialRW` / `SerialRWClient` 实现。
156
- - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),直接基于 `SerialRW` / `SerialRunner` 与 `SerialRWClient` 实现 `run / list / server` 等子命令。
179
+ - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),以 `SerialNotebook` 作为上层统一入口,并通过 `SerialRWServer`/`SerialRWClient` 实现远程 `run / list / server` 等子命令。
180
+
157
181
 
@@ -22,34 +22,41 @@ pip install sdev
22
22
 
23
23
  ## 命令行快速上手
24
24
 
25
- 安装后会提供一个 `sdev` 命令,包括:
26
-
27
- - `sdev list`:扫描本地 / 远程可用串口;
28
- - `sdev run`:在选定设备上执行命令;
29
- - `sdev server`:启动/停止本机串口共享服务;
25
+ 安装后提供一个 `sdev` 命令,核心功能包括:
26
+
27
+ - **`sdev list [scope] [filters...]`**:扫描本地 / 远程可用串口。
28
+ - `scope`: `local`, `remote` 或不填。
29
+ - `filters`: 如 `type=XC01` 或 `host=192.168.1.101`。
30
+ - `-f, --fast`: 快速模式,找到第一个符合条件的设备后立即输出(适合脚本)。
31
+ - **`sdev run [cmd...]`**:执行命令。
32
+ - `-i, --interrupt-only`: 仅发送一次 Ctrl-C,随后被动收集输出(不发送新命令)。
33
+ - `-t, --timeout`: 整体超时限制。
34
+ - `-w, --wait-forever`: 永久等待输出,直至整体超时或手动中断。
35
+ - **`sdev server [action]`**:管理本机串口共享服务。
36
+ - `action`: `start`, `stop`, `restart`, `status`。
37
+ - **`sdev mcp [action]`**:AI 集成(Model Context Protocol)。
38
+ - `action`: `install` (一键配置到 IDE), `run` (由 IDE 调起的后台服务)。
30
39
  - `sdev set` / `sdev show` / `sdev reset`:管理默认设备与缓存。
31
40
 
32
41
  典型流程:
33
42
 
34
43
  ```bash
35
- # 1. (可选)在“串口所在机器”上启动 server
44
+ # 1. 在“串口所在机器”上启动 server
36
45
  sdev server start
37
46
 
38
- # 2. 扫描可用设备
39
- sdev list
47
+ # 2. 扫描并筛选可用设备
48
+ sdev list remote # 仅扫描远程
49
+ sdev list type=XC01 # 仅列出特定型号
50
+ sdev list -f type=XC01 # 快速模式:找到第一个 XC01 立即返回(适合脚本)
40
51
 
41
- # 3. (推荐)选中一块板,配置为默认设备
52
+ # 3. 选中一块板,配置为默认设备
42
53
  sdev set default <DEVICE_ID>
43
54
 
44
- # 4. 之后即可直接执行命令
45
- sdev run 'cat /proc/meminfo'
46
- sdev run -t 10 'dmesg | tail'
55
+ # 4. 执行命令
56
+ sdev run 'cat /proc/meminfo' # 执行完整交互
57
+ sdev run -i # 仅发送 Ctrl-C (用于中断正在跑的任务并查看输出)
47
58
  ```
48
59
 
49
- 更详细的 CLI 用法与行为说明,参考项目内的 skill 文档:
50
-
51
- - `docs/skills/serial-sdev-cli/SKILL.md`
52
-
53
60
  ---
54
61
 
55
62
  ## Python API 快速上手(SerialNotebook)
@@ -90,6 +97,25 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
90
97
 
91
98
  ---
92
99
 
100
+ ## AI 集成 (MCP)
101
+
102
+ `sdev` 支持 **Model Context Protocol (MCP)**,允许 AI 助手(如 Cursor、Claude Desktop)直接操作串口,执行命令并感知设备状态。
103
+
104
+ ### 1. 获取配置信息
105
+ 在本地环境执行:
106
+ ```bash
107
+ sdev mcp install
108
+ ```
109
+ 该命令会自动生成可用于粘贴的 JSON 配置片段,并提示 Cursor 或 Claude Desktop 的配置路径。
110
+
111
+ ### 2. 提供的能力 (Tools)
112
+ 安装后,AI 助手将获得以下工具:
113
+ - `sdev_list`: 获取本地及远程可用设备列表。
114
+ - `sdev_run`: 在指定设备上执行命令并获取输出。
115
+ - `sdev_set_default`: 设置当前会话的默认设备。
116
+
117
+ ---
118
+
93
119
  ## 适用 / 不适用的场景
94
120
 
95
121
  **适用:**
@@ -108,9 +134,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
108
134
  ## 更多文档
109
135
 
110
136
  - **内部架构与设计说明**:`docs/arch_design.md`
111
- - **技能(在 Cursor 里集成使用方式)**:
112
- - `docs/skills/serial-sdev-cli/SKILL.md`
113
- - `docs/skills/serial-sdev-api/SKILL.md`
114
137
 
115
138
  源码结构概览(仅列出现行实现):
116
139
 
@@ -118,5 +141,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
118
141
  - `serial_rw.py`:本地串口读写与锁管理(`SerialRW`)、运行控制器(`SerialRunner`)、端口扫描与快速探测;
119
142
  - `serial_rw_server.py`:远程串口共享服务与客户端(`SerialRWServer` / `SerialRWClient`),以及主机发现逻辑;
120
143
  - `serial_notebook.py`:Notebook / 脚本场景下的最高层 Python 入口(`SerialNotebook`),基于 `SerialRW` / `SerialRWClient` 实现。
121
- - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),直接基于 `SerialRW` / `SerialRunner` 与 `SerialRWClient` 实现 `run / list / server` 等子命令。
144
+ - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),以 `SerialNotebook` 作为上层统一入口,并通过 `SerialRWServer`/`SerialRWClient` 实现远程 `run / list / server` 等子命令。
145
+
122
146
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdev"
7
- version = "0.7.2"
7
+ version = "0.7.5"
8
8
  description = "串口控制器工具包"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
@@ -23,12 +23,11 @@ from datetime import datetime
23
23
  from typing import Any, Dict, List, Optional, Tuple
24
24
 
25
25
  from ..modules import (
26
- SerialRW,
27
- SerialRunner,
28
26
  check_port_alive_nonblock,
29
27
  scan_serial_ports,
30
28
  DEFAULT_SERVICE_PORT,
31
29
  )
30
+ from ..modules.serial_notebook import SerialNotebook
32
31
  from ..modules.serial_rw_server import (
33
32
  SerialRWClient,
34
33
  fetch_boards_from_host,
@@ -141,9 +140,9 @@ def _read_default() -> Optional[Dict[str, Any]]:
141
140
  return None
142
141
 
143
142
 
144
- def _write_default(host: str, port: str, device_id: str) -> None:
143
+ def _write_default(host: str, device: str, device_id: str) -> None:
145
144
  with open(_default_config_path(), "w", encoding="utf-8") as f:
146
- json.dump({"host": host, "port": port, "device_id": device_id}, f, ensure_ascii=False)
145
+ json.dump({"host": host, "device": device, "device_id": device_id}, f, ensure_ascii=False)
147
146
 
148
147
 
149
148
  def _clear_default() -> None:
@@ -153,10 +152,10 @@ def _clear_default() -> None:
153
152
 
154
153
 
155
154
  def _resolve_device_to_host_port(device_id: str) -> Optional[Tuple[str, str]]:
156
- """从 list 缓存中按 device_id 解析出 (host, port);未找到返回 None。"""
155
+ """从 list 缓存中按 device_id 解析出 (host, device);未找到返回 None。"""
157
156
  for e in _read_list_cache():
158
157
  if (e.get("device_id") or "").lower() == device_id.lower():
159
- return (e.get("host", "localhost"), e.get("port", ""))
158
+ return (e.get("host", "localhost"), e.get("device", ""))
160
159
  return None
161
160
 
162
161
 
@@ -279,6 +278,14 @@ def _build_parser() -> argparse.ArgumentParser:
279
278
  help="status 时显示的最近日志行数,默认 10",
280
279
  )
281
280
 
281
+ # sdev mcp run | install
282
+ mcp_p = subparsers.add_parser("mcp", help="MCP (Model Context Protocol) 相关工具")
283
+ mcp_p.add_argument(
284
+ "action",
285
+ choices=["run", "install"],
286
+ help="run=启动 MCP 服务(stdio传输);install=获取可粘贴的配置信息",
287
+ )
288
+
282
289
  return parser
283
290
 
284
291
 
@@ -299,52 +306,60 @@ def _handle_run(args: argparse.Namespace) -> int:
299
306
  if not default_cfg:
300
307
  raise SystemExit("sdev run: 未指定 --port 且未配置默认设备,请先执行 sdev set default <device_id> 或 sdev set <host> <serial_device>")
301
308
  host = (default_cfg.get("host") or "").strip() or "localhost"
302
- port = (default_cfg.get("port") or "").strip()
303
- if not port:
304
- raise SystemExit("sdev run: 默认配置缺少 port")
309
+ device = (default_cfg.get("device") or default_cfg.get("port") or "").strip()
310
+ if not device:
311
+ raise SystemExit("sdev run: 默认配置缺少 device")
305
312
  else:
306
313
  host = "localhost"
314
+ device = port
307
315
 
308
- if (host or "").lower() == "localhost":
309
- core: Any = SerialRW(port, args.baud)
316
+ overall_timeout: Optional[float] = args.timeout
317
+ # 若未显式指定整体超时,CLI 默认给一个有限的响应超时,避免在无输出设备上永久挂起。
318
+ if args.wait_forever:
319
+ response_timeout: Optional[float] = None
310
320
  else:
311
- # 远程场景直接使用新的 SerialRWClient 内核
312
- core = SerialRWClient(host, DEFAULT_SERVICE_PORT, port, args.baud)
313
- runner = SerialRunner(core)
321
+ response_timeout = overall_timeout if overall_timeout is not None else 2.0
314
322
 
315
- overall_timeout: Optional[float] = args.timeout
316
- response_timeout: Optional[float] = None if args.wait_forever else overall_timeout
323
+ # CLI 统一通过 SerialNotebook 作为上层入口:
324
+ # - 本地:直接使用本机串口;
325
+ # - 远程:通过 SerialRWClient 由 SerialNotebook 内部接管。
326
+ if (host or "").lower() == "localhost":
327
+ nb = SerialNotebook(device=device, baudrate=args.baud)
328
+ else:
329
+ nb = SerialNotebook(device=device, baudrate=args.baud, host=host, port=DEFAULT_SERVICE_PORT)
317
330
 
318
- with core:
319
- # CLI 下每条命令(或一次 interrupt-only)前空一行,便于与上一次输出隔离;脚本/API 不经过此处,无多余空行。
320
- print()
331
+ with nb:
321
332
  try:
322
333
  if interrupt_only:
323
- # 仅发送一次中断信号,然后调用 run(cmd=None, ...) 等待提示符与后续输出。
324
- runner.send_interrupt()
325
- runner.run(
326
- None,
327
- prompt_flag=args.prompt_flag,
328
- overall_timeout=overall_timeout,
329
- response_timeout=response_timeout,
330
- wait_forever=args.wait_forever,
331
- stream=False,
332
- display=True,
333
- )
334
+ # 仅发送一次中断信号,然后在较短窗口内被动收集输出。
335
+ nb.send_interrupt()
336
+ lines = nb.wait_for_silence(timeout=response_timeout or 1.0)
337
+ # 为保持行为简单,这里仅以灰色输出剩余行,不再额外做提示符高亮。
338
+ GREY = "\033[90m"
339
+ RESET = "\033[0m"
340
+ for line in lines:
341
+ text = getattr(line, "text", str(line))
342
+ sys.stdout.write(f"{GREY}{text}{RESET}")
343
+ sys.stdout.flush()
334
344
  else:
335
345
  # 正常执行命令。
336
- runner.run(
337
- cmd_str,
338
- prompt_flag=args.prompt_flag,
346
+ ret = nb.run(
347
+ cmd=cmd_str,
348
+ end_flag=args.prompt_flag,
339
349
  overall_timeout=overall_timeout,
340
350
  response_timeout=response_timeout,
341
- wait_forever=args.wait_forever,
342
351
  stream=False,
343
- display=True,
352
+ verbose=False,
353
+ keep_type=True,
344
354
  )
355
+ for line in ret:
356
+ if line.kind == 'body' or line.kind == 'error':
357
+ sys.stdout.write(line.text)
358
+ sys.stdout.flush()
359
+ print()
345
360
  except KeyboardInterrupt:
346
361
  # 捕获本地 Ctrl-C:向开发板发送一次中断信号(Ctrl-C),尽量中断远端前台命令。
347
- runner.send_interrupt()
362
+ nb.send_interrupt()
348
363
  # 提示用户本次为主动中断(黄色)。
349
364
  YELLOW = "\033[33m"
350
365
  RESET = "\033[0m"
@@ -425,13 +440,8 @@ def _handle_list(args: argparse.Namespace) -> int:
425
440
 
426
441
  def _model_one(port: str) -> Tuple[str, str]:
427
442
  try:
428
- core = SerialRW(port, 115200)
429
- runner = SerialRunner(core)
430
- core.connect()
431
- try:
432
- return (port, runner.check_model_type(timeout=2.0) or "unknown")
433
- finally:
434
- core.disconnect()
443
+ with SerialNotebook(device=port, baudrate=115200) as nb:
444
+ return (port, nb.get_model_type(timeout=2.0, verbose=False) or "unknown")
435
445
  except Exception as e:
436
446
  return (port, f"error: {e}")
437
447
 
@@ -445,7 +455,7 @@ def _handle_list(args: argparse.Namespace) -> int:
445
455
  for port, device_type in model_results:
446
456
  entries.append({
447
457
  "host": "localhost",
448
- "port": port,
458
+ "device": port,
449
459
  "device_id": _device_id("localhost", port),
450
460
  "device_type": device_type if not device_type.startswith("error:") else "unknown",
451
461
  "last_update": now,
@@ -455,32 +465,32 @@ def _handle_list(args: argparse.Namespace) -> int:
455
465
  def _scan_remote_task() -> List[Dict[str, Any]]:
456
466
  entries: List[Dict[str, Any]] = []
457
467
  hosts = get_remote_hosts()
468
+ # print(f"[debug] remote hosts found by discovery: {hosts}")
458
469
  if not hosts:
459
470
  return entries
460
471
  for host, service_port in hosts:
472
+ # print(f"[debug] fetching boards from {host}:{service_port}...")
461
473
  boards = fetch_boards_from_host(host, service_port, timeout=10.0)
474
+ # print(f"[debug] host {host} returned {len(boards)} boards")
462
475
  for b in boards:
463
476
  if not b.get("available"):
464
477
  continue
465
- serial_port = (b.get("port") or "").strip()
466
- if not serial_port:
478
+ serial_dev = (b.get("device") or b.get("port") or "").strip()
479
+ if not serial_dev:
467
480
  continue
468
481
  baud = int(b.get("baudrate") or 115200)
469
482
  device_type = "unknown"
470
483
  try:
471
- core = SerialRWClient(host, service_port, serial_port, baud)
472
- runner = SerialRunner(core)
473
- core.connect()
474
- try:
475
- device_type = runner.check_model_type(timeout=2.0) or "unknown"
476
- finally:
477
- core.disconnect()
478
- except Exception:
484
+ with SerialNotebook(device=serial_dev, baudrate=baud, host=host, port=service_port) as nb:
485
+ device_type = nb.get_model_type(timeout=3.0, verbose=False) or "unknown"
486
+ except Exception as e:
487
+ # 如果报错,说明连接或者握手阶段就出问题了
488
+ # print(f"[debug] failed to get model type from {host}:{serial_dev}: {e}")
479
489
  device_type = "unknown"
480
490
  entries.append({
481
491
  "host": host,
482
- "port": serial_port,
483
- "device_id": _device_id(host, serial_port),
492
+ "device": serial_dev,
493
+ "device_id": _device_id(host, serial_dev),
484
494
  "device_type": device_type if not device_type.startswith("error:") else "unknown",
485
495
  "last_update": now,
486
496
  })
@@ -532,6 +542,9 @@ def _handle_list(args: argparse.Namespace) -> int:
532
542
  entries = [e for e in entries if (e.get("device_type") or "").lower() == filters["type"].lower()]
533
543
  if "host" in filters:
534
544
  entries = [e for e in entries if (e.get("host") or "").lower() == filters["host"].lower()]
545
+
546
+ # 默认展示所有探测到的设备,即便型号识别为 unknown。
547
+ # entries = [e for e in entries if (e.get("device_type") or "").lower() != "unknown"]
535
548
  if fast and len(entries) > 1:
536
549
  entries = entries[:1]
537
550
 
@@ -548,7 +561,7 @@ def _handle_list(args: argparse.Namespace) -> int:
548
561
  for e in entries:
549
562
  print(
550
563
  f"{e.get('host', ''):<{col_host}} "
551
- f"{e.get('port', ''):<{col_port}} "
564
+ f"{e.get('device', ''):<{col_port}} "
552
565
  f"{e.get('device_id', ''):<{col_id}} "
553
566
  f"{e.get('device_type', 'unknown'):<{col_type}} "
554
567
  f"{e.get('last_update', ''):<{col_time}}"
@@ -726,7 +739,7 @@ def _handle_reset(args: argparse.Namespace) -> int:
726
739
  if _server_is_running() is not None:
727
740
  _handle_server_stop()
728
741
  _clear_default()
729
- list_path = os.path.join(_sdev_cache_dir(), "list_cache.json")
742
+ list_path = _list_cache_path()
730
743
  if os.path.isfile(list_path):
731
744
  os.remove(list_path)
732
745
  print(f"[{_ts()}] list cache cleared")
@@ -734,6 +747,63 @@ def _handle_reset(args: argparse.Namespace) -> int:
734
747
  return 0
735
748
 
736
749
 
750
+ def _handle_mcp(args: argparse.Namespace) -> int:
751
+ action = getattr(args, "action", None)
752
+ if action == "run":
753
+ # 启动 MCP 服务
754
+ from ..modules.mcp_server import mcp
755
+ mcp.run()
756
+ return 0
757
+ if action == "install":
758
+ return _mcp_show_config()
759
+ return 1
760
+
761
+
762
+ def _mcp_show_config() -> int:
763
+ import platform
764
+ import json
765
+
766
+ # 确定 Python 路径
767
+ py_path = sys.executable
768
+ # 确定项目根目录(用于 PYTHONPATH)
769
+ project_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
770
+
771
+ # 封装配置对象
772
+ sdev_config = {
773
+ "command": py_path,
774
+ "args": ["-m", "sdev.modules.mcp_server"],
775
+ "env": {
776
+ "PYTHONPATH": project_root
777
+ }
778
+ }
779
+
780
+ system = platform.system()
781
+ home = os.path.expanduser("~")
782
+
783
+ print(f"\n[{_ts()}] === SDEV MCP 配置指南 ===")
784
+
785
+ # 1. 针对 Cursor
786
+ print(f"\n[Cursor 推荐路径]:")
787
+ if system == "Linux":
788
+ print(f" {home}/.cursor/mcp.json")
789
+ elif system == "Darwin":
790
+ print(f" ~/Library/Application Support/Cursor/User/globalStorage/rosebud.cursor-ide-browser/mcp.json")
791
+
792
+ print(f"\n[Cursor/Claude 粘贴内容 (sdev 字段)]: ")
793
+ print(json.dumps(sdev_config, indent=2, ensure_ascii=False))
794
+
795
+ print(f"\n[完整示例 (mcp.json)]: ")
796
+ full_example = {
797
+ "mcpServers": {
798
+ "sdev": sdev_config
799
+ }
800
+ }
801
+ print(json.dumps(full_example, indent=2, ensure_ascii=False))
802
+
803
+ print(f"\n[{_ts()}] 请将上述内容添加到你的 MCP 配置文件中,并重启 AI 客户端。")
804
+ return 0
805
+
806
+
737
807
  def main(argv: Optional[list[str]] = None) -> int:
738
808
  parser = _build_parser()
739
809
  args = parser.parse_args(argv)
@@ -750,6 +820,8 @@ def main(argv: Optional[list[str]] = None) -> int:
750
820
  return _handle_reset(args)
751
821
  if args.command == "server":
752
822
  return _handle_server(args)
823
+ if args.command == "mcp":
824
+ return _handle_mcp(args)
753
825
 
754
826
  parser.error(f"未知子命令: {args.command}")
755
827
  return 1
@@ -105,17 +105,15 @@ class Demoboard:
105
105
  response_timeout = None if wait_forever else timeout
106
106
 
107
107
  def _iter_cmd() -> Generator[str, None, None]:
108
- for line in self._nb.run(
108
+ return self._nb.run(
109
109
  cmd=cmd,
110
110
  end_flag=flag,
111
111
  overall_timeout=timeout,
112
112
  response_timeout=response_timeout,
113
113
  stream=True,
114
114
  verbose=True,
115
- ):
116
- text = getattr(line, "text", str(line))
117
- yield text
118
-
115
+ )
116
+
119
117
  if stream:
120
118
  return _iter_cmd()
121
119
  return list(_iter_cmd())
@@ -1,12 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from .serial_rw import SerialRW, scan_serial_ports, check_port_alive_nonblock
4
- from .serial_rw_server import SerialRWClient, SerialRWServer, DEFAULT_SERVICE_PORT
3
+ from .serial_rw import scan_serial_ports, check_port_alive_nonblock
4
+ from .serial_rw_server import DEFAULT_SERVICE_PORT, SerialRWServer
5
5
  from .serial_notebook import SerialNotebook
6
6
 
7
7
  __all__ = [
8
- "SerialRW",
9
- "SerialRWClient",
8
+ "SerialNotebook",
10
9
  "SerialRWServer",
11
10
  "scan_serial_ports",
12
11
  "check_port_alive_nonblock",
@@ -0,0 +1,124 @@
1
+ from mcp.server.fastmcp import FastMCP
2
+ from .serial_notebook import SerialNotebook
3
+ from .serial_rw_server import get_remote_hosts, fetch_boards_from_host, DEFAULT_SERVICE_PORT
4
+ from .serial_rw import scan_serial_ports, check_port_alive_nonblock
5
+ import logging
6
+ import os
7
+ import sys
8
+
9
+ # 初始化 FastMCP
10
+ mcp = FastMCP("sdev")
11
+
12
+ @mcp.tool()
13
+ def sdev_list(scope: str = "all") -> str:
14
+ """
15
+ 获取可用串口设备列表。
16
+ scope: "all" (默认), "local" (仅本地), "remote" (仅远程)。
17
+ 返回格式化的设备列表字符串。
18
+ """
19
+ from ..cli import _device_id
20
+
21
+ local_ports = []
22
+ if scope in ("all", "local"):
23
+ local_ports = scan_serial_ports()
24
+
25
+ remote_hosts = []
26
+ if scope in ("all", "remote"):
27
+ try:
28
+ remote_hosts = get_remote_hosts()
29
+ except:
30
+ pass
31
+
32
+ results = []
33
+
34
+ # 本地扫描
35
+ for p in local_ports:
36
+ r = check_port_alive_nonblock(p, timeout=1.0)
37
+ if r.get("available"):
38
+ # 尝试读型号
39
+ try:
40
+ with SerialNotebook(device=p) as nb:
41
+ model = nb.get_model_type(timeout=2.0) or "unknown"
42
+ except:
43
+ model = "unknown"
44
+ results.append(f"HOST: localhost, DEVICE: {p}, ID: {_device_id('localhost', p)}, TYPE: {model}")
45
+
46
+ # 远程扫描
47
+ for host, port in remote_hosts:
48
+ try:
49
+ boards = fetch_boards_from_host(host, port)
50
+ for b in boards:
51
+ if b.get("available"):
52
+ dev = b.get("device") or b.get("port")
53
+ try:
54
+ with SerialNotebook(device=dev, host=host, port=port) as nb:
55
+ model = nb.get_model_type(timeout=2.0) or "unknown"
56
+ except:
57
+ model = "unknown"
58
+ results.append(f"HOST: {host}, DEVICE: {dev}, ID: {_device_id(host, dev)}, TYPE: {model}")
59
+ except:
60
+ continue
61
+
62
+ if not results:
63
+ return "未发现可用设备。"
64
+ return "\n".join(results)
65
+
66
+ @mcp.tool()
67
+ def sdev_run(cmd: str, device_id: str = None, timeout: float = 10.0) -> str:
68
+ """
69
+ 在指定设备上执行命令。
70
+ cmd: 要执行的 shell 命令。
71
+ device_id: 设备 ID(从 sdev_list 获取)。如果不指定,则尝试使用默认设备。
72
+ timeout: 整体超时时间(秒)。
73
+ """
74
+ from ..cli import _read_default, _resolve_device_to_host_port
75
+
76
+ host = "localhost"
77
+ device = None
78
+
79
+ if device_id:
80
+ res = _resolve_device_to_host_port(device_id)
81
+ if res:
82
+ host, device = res
83
+ else:
84
+ return f"错误:未找到 ID 为 {device_id} 的设备。"
85
+ else:
86
+ cfg = _read_default()
87
+ if cfg:
88
+ host = cfg.get("host", "localhost")
89
+ device = cfg.get("device") or cfg.get("port")
90
+ else:
91
+ return "错误:未指定 device_id 且没有配置默认设备。"
92
+
93
+ if not device:
94
+ return "错误:无法确定串口路径。"
95
+
96
+ try:
97
+ # 使用 SerialNotebook 执行
98
+ if host == "localhost":
99
+ nb = SerialNotebook(device=device)
100
+ else:
101
+ nb = SerialNotebook(device=device, host=host, port=DEFAULT_SERVICE_PORT)
102
+
103
+ with nb:
104
+ lines = nb.run(cmd, overall_timeout=timeout, stream=False, verbose=False)
105
+ return "".join(lines).strip()
106
+ except Exception as e:
107
+ return f"执行失败:{e}"
108
+
109
+ @mcp.tool()
110
+ def sdev_set_default(device_id: str) -> str:
111
+ """
112
+ 设置默认操作的设备。
113
+ device_id: 设备 ID。
114
+ """
115
+ from ..cli import _resolve_device_to_host_port, _write_default
116
+ res = _resolve_device_to_host_port(device_id)
117
+ if not res:
118
+ return f"错误:未找到 ID 为 {device_id} 的设备。"
119
+ host, device = res
120
+ _write_default(host, device, device_id)
121
+ return f"成功:已将默认设备设置为 {host}:{device} (ID: {device_id})"
122
+
123
+ if __name__ == "__main__":
124
+ mcp.run()
@@ -3,7 +3,7 @@ import sys
3
3
  import time
4
4
  from loguru import logger
5
5
 
6
- from .serial_rw import SerialRW, SerialLine, CTRL_C
6
+ from .serial_rw import SerialLine, CTRL_C
7
7
 
8
8
  GREY = "\033[90m"
9
9
  YELLOW = "\033[33m"
@@ -13,9 +13,10 @@ RESET = "\033[0m"
13
13
  class SerialNotebook():
14
14
  def __init__(self, device: str, baudrate: int = 115200, host=None, port=None, default_flag=" #"):
15
15
  if host is not None and port is not None:
16
- from serial_rw_server import SerialRWClient
16
+ from .serial_rw_server import SerialRWClient
17
17
  self._core = SerialRWClient(host, port, device, baudrate)
18
18
  else:
19
+ from .serial_rw import SerialRW
19
20
  self._core = SerialRW(device, baudrate)
20
21
  self.default_flag = default_flag
21
22
  self._muted = False
@@ -40,7 +41,7 @@ class SerialNotebook():
40
41
  def __exit__(self, exc_type, exc_value, traceback):
41
42
  self._core.disconnect()
42
43
 
43
- def run(self, cmd: str, end_flag=None, allow_fold=True, max_fold_lines=4, strip_echo=True, stream=False, verbose=True, overall_timeout=None, response_timeout=None):
44
+ def run(self, cmd: str, end_flag=None, allow_fold=True, max_fold_lines=4, strip_echo=True, stream=False, verbose=True, overall_timeout=None, response_timeout=None, keep_type=False):
44
45
  # 先清理残留输出,避免前一次命令的尾巴影响本次结果。
45
46
  assert cmd is not None
46
47
  self._core.drain_buffer()
@@ -73,12 +74,15 @@ class SerialNotebook():
73
74
  ):
74
75
  _check_overall_timeout()
75
76
  line: SerialLine
76
- if line.kind == 'timeout':
77
+ if line.kind == 'error' and line.text == '<sdev timeout>\n':
77
78
  raise TimeoutError
78
79
  # print(line.kind, line.line, show_log, isinstance(line, SerialLine), line.kind == 'body')
79
80
  if show_log and isinstance(line, SerialLine) and line.kind == 'body':
80
81
  self._write_raw(f"{GREY}{line.text}{RESET}")
81
- yield line
82
+ if keep_type:
83
+ yield line
84
+ else:
85
+ yield line.text
82
86
  # 整个命令结束后,如启用 display,则额外输出一个空行(两个换行),
83
87
  # 便于与后续命令在视觉上明显隔离。
84
88
  if show_log:
@@ -137,12 +141,27 @@ class SerialNotebook():
137
141
  self.run(cmd="reboot", end_flag=reboot_finish_when_see)
138
142
 
139
143
 
140
- def get_model_type(self, prompt_flag=" #", timeout=2.0, verbose=False) -> bool:
144
+ def get_model_type(self, prompt_flag=None, timeout=3.0, verbose=False):
145
+ """
146
+ 读取设备树型号前缀:
147
+ - 命令:cat /proc/device-tree/model 2>/dev/null; echo
148
+ - 若 prompt_flag 为 None,则尝试使用较宽泛的匹配。
149
+ """
150
+ if prompt_flag is None:
151
+ # 常见提示符结尾:#, $, >
152
+ prompt_flag = "[#$>]"
141
153
  try:
142
154
  cmd = "cat /proc/device-tree/model 2>/dev/null; echo"
143
- lines = self.run(cmd=cmd, end_flag=prompt_flag, stream=False, overall_timeout=timeout, verbose=verbose)
144
- if lines and len(lines) > 2:
145
- return lines[-2].text.strip()
155
+ lines = self.run(
156
+ cmd=cmd,
157
+ end_flag=prompt_flag,
158
+ stream=False,
159
+ overall_timeout=timeout,
160
+ response_timeout=timeout,
161
+ verbose=verbose,
162
+ )
163
+ if lines and len(lines) >= 2:
164
+ return lines[-2].strip()
146
165
  except Exception:
147
166
  pass
148
167
  return None
@@ -105,9 +105,17 @@ def _content_contains_ctrl_c(text: str) -> bool:
105
105
  return CTRL_C in n or "^C" in n
106
106
 
107
107
  def scan_serial_ports() -> list[str]:
108
- """扫描系统可用串口。"""
108
+ """扫描系统可用串口,并按平台做基础筛选。
109
+
110
+ - POSIX:仅保留以 /dev/tty 开头的设备(忽略 /dev/cu.* 等非目标设备);
111
+ - 其他平台:直接返回 list_ports 结果。
112
+ """
109
113
  import serial.tools.list_ports
110
- return [p.device for p in serial.tools.list_ports.comports()]
114
+
115
+ ports = [p.device for p in serial.tools.list_ports.comports()]
116
+ if os.name == "posix":
117
+ ports = [d for d in ports if d.startswith("/dev/tty")]
118
+ return ports
111
119
 
112
120
  def check_port_alive_nonblock(port: str, baudrate: int = 115200, timeout: float = 1.0) -> dict:
113
121
  """快速探测串口是否可用(不阻塞)。"""
@@ -244,7 +252,7 @@ class SerialRW():
244
252
  for line in self._flag_stopper(lines, flag, allow_fold, max_fold_lines):
245
253
  yield SerialLine(kind='body', text=line)
246
254
  except TimeoutError:
247
- yield SerialLine(kind='timeout', text='<sdev timeout>\n')
255
+ yield SerialLine(kind='error', text='<sdev timeout>\n')
248
256
 
249
257
  def drain_buffer(self) -> None:
250
258
  while True:
@@ -9,6 +9,8 @@ import time
9
9
  from concurrent.futures import ThreadPoolExecutor, as_completed
10
10
  from typing import Any, Generator, Optional, List, Tuple, Dict
11
11
 
12
+ from loguru import logger
13
+
12
14
  from .serial_rw import SerialRW, SerialLine, scan_serial_ports, check_port_alive_nonblock
13
15
 
14
16
  DEFAULT_SERVICE_PORT = 7000
@@ -218,6 +220,7 @@ class SerialRWServer:
218
220
  if isinstance(init, dict):
219
221
  cmd = init.get("cmd")
220
222
  if cmd == "list":
223
+ logger.info(f"[server] list request from {addr}")
221
224
  ports = scan_serial_ports()
222
225
  boards: List[dict] = []
223
226
  if ports:
@@ -226,14 +229,18 @@ class SerialRWServer:
226
229
  with ThreadPoolExecutor(max_workers=min(8, len(ports))) as ex:
227
230
  futs = {ex.submit(_probe, p): p for p in ports}
228
231
  for fut in as_completed(futs):
229
- r = fut.result()
230
- boards.append({
231
- "port": r["port"],
232
- "baudrate": r["baudrate"],
233
- "available": r["available"],
234
- "reason": r.get("reason", ""),
235
- })
232
+ try:
233
+ r = fut.result()
234
+ boards.append({
235
+ "port": r["port"],
236
+ "baudrate": r["baudrate"],
237
+ "available": r["available"],
238
+ "reason": r.get("reason", ""),
239
+ })
240
+ except Exception as e:
241
+ logger.error(f"[server] probe error for port: {e}")
236
242
  conn.sendall(json.dumps({"boards": boards}, ensure_ascii=False).encode("utf-8") + b"\n")
243
+ logger.info(f"[server] list response sent to {addr} (count={len(boards)})")
237
244
  return
238
245
  if cmd == "probe":
239
246
  port = (init.get("device") or init.get("port") or "").strip()
@@ -251,10 +258,13 @@ class SerialRWServer:
251
258
  conn.sendall(b'{"ok":false,"error":"missing device"}\n')
252
259
  return
253
260
 
261
+ logger.info(f"[server] session from {addr}: port={device_val}, baudrate={baudrate_val}")
254
262
  dev = SerialRW(device_val, baudrate_val)
255
263
  try:
256
264
  dev.connect()
265
+ logger.info(f"[server] connected to {device_val} for {addr}")
257
266
  except Exception as e:
267
+ logger.error(f"[server] connect failed for {device_val}: {e}")
258
268
  err = json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False).encode("utf-8") + b"\n"
259
269
  conn.sendall(err)
260
270
  return
@@ -340,6 +350,7 @@ class SerialRWServer:
340
350
 
341
351
  while True:
342
352
  conn, addr = sock.accept()
353
+ logger.info(f"[server] TCP connection from {addr}")
343
354
  threading.Thread(target=self._session_loop, args=(conn, addr), daemon=True).start()
344
355
 
345
356
  @staticmethod
@@ -401,17 +412,51 @@ def discover_hosts_via_zeroconf(timeout: float = 0.8) -> List[Tuple[str, int]]:
401
412
  return []
402
413
 
403
414
  def discover_hosts_via_udp(timeout: float = 1.5) -> List[Tuple[str, int]]:
415
+ """
416
+ 通过 UDP 广播 / 定向探测发现远程 sdev server:
417
+ - 优先读取环境变量 SDEV_DISCOVERY_TARGETS="host[:port],host2[:port2],..."
418
+ 按指定目标发送 discovery 报文;
419
+ - 未指定时,退回到对 255.255.255.255:DISCOVERY_UDP_PORT 的广播。
420
+ """
404
421
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
405
422
  try:
406
423
  sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
407
424
  sock.settimeout(timeout)
408
425
  sock.bind(("", 0))
409
- sock.sendto(b"SDEV_DISCOVERY", ("255.255.255.255", DISCOVERY_UDP_PORT))
426
+
427
+ # 解析 SDEV_DISCOVERY_TARGETS 作为定向探测目标
428
+ targets: List[Tuple[str, int]] = []
429
+ raw = os.environ.get("SDEV_DISCOVERY_TARGETS", "").strip()
430
+ if raw:
431
+ for item in raw.split(","):
432
+ item = item.strip()
433
+ if not item:
434
+ continue
435
+ if ":" in item:
436
+ h, ps = item.rsplit(":", 1)
437
+ try:
438
+ targets.append((h.strip(), int(ps)))
439
+ except ValueError:
440
+ targets.append((h.strip(), DISCOVERY_UDP_PORT))
441
+ else:
442
+ targets.append((item, DISCOVERY_UDP_PORT))
443
+ if not targets:
444
+ targets.append(("255.255.255.255", DISCOVERY_UDP_PORT))
445
+
446
+ for host, port in targets:
447
+ try:
448
+ sock.sendto(b"SDEV_DISCOVERY", (host, port))
449
+ except OSError:
450
+ continue
451
+
410
452
  hosts: set = set()
411
453
  deadline = time.monotonic() + timeout
412
454
  while time.monotonic() < deadline:
413
455
  try:
456
+ sock.settimeout(max(0.0, deadline - time.monotonic()))
414
457
  data, addr = sock.recvfrom(4096)
458
+ if not data:
459
+ continue
415
460
  obj = json.loads(data.decode("utf-8"))
416
461
  if obj.get("magic") == "sdev-remote-v1":
417
462
  hosts.add((addr[0], int(obj.get("port") or DEFAULT_SERVICE_PORT)))
@@ -454,13 +499,46 @@ def get_remote_hosts(service_port: int = DEFAULT_SERVICE_PORT) -> List[Tuple[str
454
499
  return list(merged)
455
500
 
456
501
 
457
- if __name__ == "__main__":
502
+ def _server_log_path() -> str:
503
+ """
504
+ 与 CLI `_server_log_path` 保持一致:
505
+ - 优先使用 XDG_RUNTIME_DIR/sdev/server.log
506
+ - 否则使用 ~/.cache/sdev/server.log
507
+ """
508
+ base = os.environ.get("XDG_RUNTIME_DIR") or os.path.join(os.path.expanduser("~"), ".cache")
509
+ d = os.path.join(base, "sdev")
510
+ os.makedirs(d, mode=0o700, exist_ok=True)
511
+ return os.path.join(d, "server.log")
512
+
513
+
514
+ def main() -> int:
458
515
  import sys
516
+
459
517
  port = DEFAULT_SERVICE_PORT
460
518
  if len(sys.argv) > 1:
461
519
  try:
462
520
  port = int(sys.argv[1])
463
521
  except ValueError:
464
522
  pass
523
+
524
+ # 与旧实现保持一致的日志行为:只写到 server.log,不打印到前台。
525
+ log_path = _server_log_path()
526
+ logger.remove()
527
+ logger.add(
528
+ log_path,
529
+ rotation="1 MB",
530
+ retention=5,
531
+ encoding="utf-8",
532
+ enqueue=True,
533
+ backtrace=False,
534
+ diagnose=False,
535
+ )
536
+ logger.info(f"[server] listening TCP 0.0.0.0:{port}, UDP 0.0.0.0:{DISCOVERY_UDP_PORT}")
537
+
465
538
  server = SerialRWServer(port=port)
466
539
  server.serve_forever()
540
+ return 0
541
+
542
+
543
+ if __name__ == "__main__":
544
+ raise SystemExit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdev
3
- Version: 0.7.2
3
+ Version: 0.7.5
4
4
  Summary: 串口控制器工具包
5
5
  Home-page: https://github.com/klrc/sdev
6
6
  Author: klrc
@@ -57,34 +57,41 @@ pip install sdev
57
57
 
58
58
  ## 命令行快速上手
59
59
 
60
- 安装后会提供一个 `sdev` 命令,包括:
61
-
62
- - `sdev list`:扫描本地 / 远程可用串口;
63
- - `sdev run`:在选定设备上执行命令;
64
- - `sdev server`:启动/停止本机串口共享服务;
60
+ 安装后提供一个 `sdev` 命令,核心功能包括:
61
+
62
+ - **`sdev list [scope] [filters...]`**:扫描本地 / 远程可用串口。
63
+ - `scope`: `local`, `remote` 或不填。
64
+ - `filters`: 如 `type=XC01` 或 `host=192.168.1.101`。
65
+ - `-f, --fast`: 快速模式,找到第一个符合条件的设备后立即输出(适合脚本)。
66
+ - **`sdev run [cmd...]`**:执行命令。
67
+ - `-i, --interrupt-only`: 仅发送一次 Ctrl-C,随后被动收集输出(不发送新命令)。
68
+ - `-t, --timeout`: 整体超时限制。
69
+ - `-w, --wait-forever`: 永久等待输出,直至整体超时或手动中断。
70
+ - **`sdev server [action]`**:管理本机串口共享服务。
71
+ - `action`: `start`, `stop`, `restart`, `status`。
72
+ - **`sdev mcp [action]`**:AI 集成(Model Context Protocol)。
73
+ - `action`: `install` (一键配置到 IDE), `run` (由 IDE 调起的后台服务)。
65
74
  - `sdev set` / `sdev show` / `sdev reset`:管理默认设备与缓存。
66
75
 
67
76
  典型流程:
68
77
 
69
78
  ```bash
70
- # 1. (可选)在“串口所在机器”上启动 server
79
+ # 1. 在“串口所在机器”上启动 server
71
80
  sdev server start
72
81
 
73
- # 2. 扫描可用设备
74
- sdev list
82
+ # 2. 扫描并筛选可用设备
83
+ sdev list remote # 仅扫描远程
84
+ sdev list type=XC01 # 仅列出特定型号
85
+ sdev list -f type=XC01 # 快速模式:找到第一个 XC01 立即返回(适合脚本)
75
86
 
76
- # 3. (推荐)选中一块板,配置为默认设备
87
+ # 3. 选中一块板,配置为默认设备
77
88
  sdev set default <DEVICE_ID>
78
89
 
79
- # 4. 之后即可直接执行命令
80
- sdev run 'cat /proc/meminfo'
81
- sdev run -t 10 'dmesg | tail'
90
+ # 4. 执行命令
91
+ sdev run 'cat /proc/meminfo' # 执行完整交互
92
+ sdev run -i # 仅发送 Ctrl-C (用于中断正在跑的任务并查看输出)
82
93
  ```
83
94
 
84
- 更详细的 CLI 用法与行为说明,参考项目内的 skill 文档:
85
-
86
- - `docs/skills/serial-sdev-cli/SKILL.md`
87
-
88
95
  ---
89
96
 
90
97
  ## Python API 快速上手(SerialNotebook)
@@ -125,6 +132,25 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
125
132
 
126
133
  ---
127
134
 
135
+ ## AI 集成 (MCP)
136
+
137
+ `sdev` 支持 **Model Context Protocol (MCP)**,允许 AI 助手(如 Cursor、Claude Desktop)直接操作串口,执行命令并感知设备状态。
138
+
139
+ ### 1. 获取配置信息
140
+ 在本地环境执行:
141
+ ```bash
142
+ sdev mcp install
143
+ ```
144
+ 该命令会自动生成可用于粘贴的 JSON 配置片段,并提示 Cursor 或 Claude Desktop 的配置路径。
145
+
146
+ ### 2. 提供的能力 (Tools)
147
+ 安装后,AI 助手将获得以下工具:
148
+ - `sdev_list`: 获取本地及远程可用设备列表。
149
+ - `sdev_run`: 在指定设备上执行命令并获取输出。
150
+ - `sdev_set_default`: 设置当前会话的默认设备。
151
+
152
+ ---
153
+
128
154
  ## 适用 / 不适用的场景
129
155
 
130
156
  **适用:**
@@ -143,9 +169,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
143
169
  ## 更多文档
144
170
 
145
171
  - **内部架构与设计说明**:`docs/arch_design.md`
146
- - **技能(在 Cursor 里集成使用方式)**:
147
- - `docs/skills/serial-sdev-cli/SKILL.md`
148
- - `docs/skills/serial-sdev-api/SKILL.md`
149
172
 
150
173
  源码结构概览(仅列出现行实现):
151
174
 
@@ -153,5 +176,6 @@ with SerialNotebook(device=SERIAL_PORT, baudrate=115200, host=HOST, port=7000) a
153
176
  - `serial_rw.py`:本地串口读写与锁管理(`SerialRW`)、运行控制器(`SerialRunner`)、端口扫描与快速探测;
154
177
  - `serial_rw_server.py`:远程串口共享服务与客户端(`SerialRWServer` / `SerialRWClient`),以及主机发现逻辑;
155
178
  - `serial_notebook.py`:Notebook / 脚本场景下的最高层 Python 入口(`SerialNotebook`),基于 `SerialRW` / `SerialRWClient` 实现。
156
- - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),直接基于 `SerialRW` / `SerialRunner` 与 `SerialRWClient` 实现 `run / list / server` 等子命令。
179
+ - `sdev/cli/`:命令行入口 `sdev`(`__init__.py` 中的 `main()`),以 `SerialNotebook` 作为上层统一入口,并通过 `SerialRWServer`/`SerialRWClient` 实现远程 `run / list / server` 等子命令。
180
+
157
181
 
@@ -14,6 +14,7 @@ sdev/cli/__init__.py
14
14
  sdev/cli/__main__.py
15
15
  sdev/deprecated/demoboard.py
16
16
  sdev/modules/__init__.py
17
+ sdev/modules/mcp_server.py
17
18
  sdev/modules/serial_notebook.py
18
19
  sdev/modules/serial_rw.py
19
20
  sdev/modules/serial_rw_server.py
@@ -5,7 +5,7 @@ with open("README.md", "r", encoding="utf-8") as fh:
5
5
 
6
6
  setup(
7
7
  name="sdev",
8
- version="0.7.2",
8
+ version="0.7.5",
9
9
  author="klrc",
10
10
  author_email="144069824@qq.com",
11
11
  description="串口控制器工具包",
@@ -32,6 +32,7 @@ setup(
32
32
  "pyserial>=3.5",
33
33
  "loguru>=0.6.0",
34
34
  "zeroconf>=0.39.0",
35
+ "mcp>=1.0.0",
35
36
  ],
36
37
  entry_points={
37
38
  "console_scripts": ["sdev=sdev.cli:main"],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes