s7_server.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. from __future__ import annotations
  2. from typing import Any
  3. from .collector_api import (
  4. create_s7_device as api_create_s7_device,
  5. create_s7_point as api_create_s7_point,
  6. edit_s7_device as api_edit_s7_device,
  7. edit_s7_point as api_edit_s7_point,
  8. )
  9. from .gateway_api import (
  10. s7_connect_scan as api_s7_connect_scan,
  11. s7_point_collect_test as api_s7_point_collect_test,
  12. s7_raw_read as api_s7_raw_read,
  13. )
  14. from .mcp_app import mcp
  15. @mcp.tool(
  16. name="s7.raw_read",
  17. description=(
  18. "通过采集网关对 Siemens S7 TCP 设备执行原始字节读取。调用 "
  19. "{base_url}/api/dc-gateway/s7/read。请求字段使用网关/汇采一致命名:"
  20. "ip 为 PLC 地址,port 默认 102,rock 为机架号,slot 为槽号,"
  21. "device_type 为设备类型,默认 S7-1200,可传 S7-1200、S7-Smart200。"
  22. "tsap_conn_type 可选 PG、OP、BASIC;未传时 S7-1200 使用 PG,"
  23. "device_type=S7-Smart200 时使用 OP。read 必须包含 area、start、size;"
  24. "area 可用 DB、M、I、Q,S7-Smart200 还支持 V;area=DB 时 read.db 必须大于 0。"
  25. "该接口不解析业务值,只返回语义化 communication:S7_CONNECT、S7_CONNECTED、"
  26. "S7_READ、原始十六进制字节、S7_DISCONNECT 或 S7_ERROR。"
  27. "响应透传上游 JSON,code=0 表示业务成功,code!=0 时 msg 为失败原因。"
  28. ),
  29. )
  30. def s7_raw_read(
  31. project_key: str,
  32. ip: str,
  33. read: dict[str, Any],
  34. rock: int = 0,
  35. slot: int = 1,
  36. device_type: str = "S7-1200",
  37. port: int = 102,
  38. tsap_conn_type: str | None = None,
  39. ) -> dict[str, Any]:
  40. return api_s7_raw_read(
  41. project_key,
  42. ip=ip,
  43. rock=rock,
  44. slot=slot,
  45. read=read,
  46. device_type=device_type,
  47. port=port,
  48. tsap_conn_type=tsap_conn_type,
  49. )
  50. @mcp.tool(
  51. name="s7.point_collect_test",
  52. description=(
  53. "通过采集网关读取 Siemens S7 TCP 点位并转换为业务值。调用 "
  54. "{base_url}/api/dc-gateway/s7/read_points。请求字段使用网关/汇采一致命名:"
  55. "ip 为 PLC 地址,port 默认 102,rock 为机架号,slot 为槽号,"
  56. "device_type 为设备类型,默认 S7-1200,可传 S7-1200、S7-Smart200。"
  57. "tsap_conn_type 可选 PG、OP、BASIC;未传时 S7-1200 使用 PG,"
  58. "device_type=S7-Smart200 时使用 OP。points 为点位数组,每个点位必须包含 "
  59. "area、start、type;area 可用 DB、M、I、Q,S7-Smart200 还支持 V;"
  60. "area=DB 时 db 必须大于 0。"
  61. "type 可用 bool、byte、int8、int16、uint16、int32、uint32、int64、uint64、"
  62. "float32、float64;bool 点可传 bit 读取指定 0..7 位,非 bool 点不能传 bit。"
  63. "S7 多字节数值按大端解析,响应 data.points[].value 为转换后的业务值,"
  64. "data.communication 为语义化连接/读取/断开记录。响应透传上游 JSON,code=0 表示业务成功。"
  65. ),
  66. )
  67. def s7_point_collect_test(
  68. project_key: str,
  69. ip: str,
  70. rock: int,
  71. slot: int,
  72. points: list[dict[str, Any]],
  73. device_type: str = "S7-1200",
  74. port: int = 102,
  75. tsap_conn_type: str | None = None,
  76. ) -> dict[str, Any]:
  77. return api_s7_point_collect_test(
  78. project_key,
  79. ip=ip,
  80. rock=rock,
  81. slot=slot,
  82. points=points,
  83. device_type=device_type,
  84. port=port,
  85. tsap_conn_type=tsap_conn_type,
  86. )
  87. @mcp.tool(
  88. name="s7.connect_scan",
  89. description=(
  90. "通过采集网关扫描 Siemens S7 TCP 设备可用连接组合。调用 "
  91. "{base_url}/api/dc-gateway/s7/connect_scan。只需要传 PLC 的 ip,网关固定使用端口 102,"
  92. "扫描 rock=0/1/2、slot=0/1/2、tsap_conn_type=PG/OP/BASIC 共 27 种组合。"
  93. "响应 data.available 只返回可连接成功的组合,每项包含 rock、slot、tsap_conn_type;"
  94. "如果没有可用组合则 available 为空数组。响应透传上游 JSON,code=0 表示扫描完成。"
  95. ),
  96. )
  97. def s7_connect_scan(project_key: str, ip: str) -> dict[str, Any]:
  98. return api_s7_connect_scan(project_key, ip=ip)
  99. def _resolve_collector_s7_tsap_conn_type(device_type: int, tsap_conn_type: str | None) -> str:
  100. normalized_tsap = str(tsap_conn_type or "").strip().upper()
  101. if normalized_tsap:
  102. return normalized_tsap
  103. if int(device_type) == 3:
  104. return "OP"
  105. return "PG"
  106. @mcp.tool(
  107. name="collector.s7_device_create",
  108. description=(
  109. "汇采-创建 S7 设备。调用 {data_collector_base_url}/api/collector/device,"
  110. "与 Modbus 创建设备共用地址,但请求体固定 type=s7,入参使用 S7 字段。"
  111. "创建设备必须传 name 名称、ip IP 地址、rock 机架号(轨道号)、slot 槽号;"
  112. "port 默认 102,device_type 默认 1。tsap_conn_type 可选 PG、OP、BASIC;"
  113. "未传时 device_type=1 使用 PG,device_type=3 时使用 OP。"
  114. "默认参数: is_persistent=false, "
  115. "device_group_id=0, timeout=3, alarm_interval=90, collect_interval=5。"
  116. "device_type: 1=S7-1200, 2=S7-1500, 3=S7-Smart200。"
  117. "tsap_conn_type 可用 PG、OP、BASIC。响应透传上游 JSON,state=0 表示业务成功。"
  118. ),
  119. )
  120. def collector_s7_device_create(
  121. project_key: str,
  122. name: str,
  123. ip: str,
  124. rock: int,
  125. slot: int,
  126. port: int = 102,
  127. device_type: int = 1,
  128. tsap_conn_type: str | None = None,
  129. is_persistent: bool = False,
  130. device_group_id: int = 0,
  131. timeout: int = 3,
  132. alarm_interval: int = 90,
  133. collect_interval: int = 5,
  134. ) -> dict[str, Any]:
  135. return api_create_s7_device(
  136. project_key,
  137. {
  138. "name": name,
  139. "ip": ip,
  140. "rock": rock,
  141. "slot": slot,
  142. "port": port,
  143. "device_type": device_type,
  144. "tsap_conn_type": _resolve_collector_s7_tsap_conn_type(device_type, tsap_conn_type),
  145. "is_persistent": is_persistent,
  146. "device_group_id": device_group_id,
  147. "timeout": timeout,
  148. "alarm_interval": alarm_interval,
  149. "collect_interval": collect_interval,
  150. },
  151. )
  152. @mcp.tool(
  153. name="collector.s7_device_edit",
  154. description=(
  155. "汇采-编辑 S7 设备。调用 {data_collector_base_url}/api/collector/s7/device/update。"
  156. "必须传 ori_id 原设备 id、name、ip、rock、slot;port 默认 102,device_type 默认 1,"
  157. "tsap_conn_type 可选 PG、OP、BASIC;未传时 device_type=1 使用 PG,device_type=3 时使用 OP。"
  158. "编辑前设备不能处于已连接状态;若已连接,请先调用 "
  159. "collector.device_disconnect,并传 device_type=s7。该接口是全量更新语义。"
  160. "默认参数: is_persistent=false, device_group_id=0, timeout=3, alarm_interval=90, "
  161. "collect_interval=5。device_type: 1=S7-1200, 2=S7-1500, 3=S7-Smart200。"
  162. "tsap_conn_type 可用 PG、OP、BASIC。响应透传上游 JSON,state=0 表示业务成功。"
  163. ),
  164. )
  165. def collector_s7_device_edit(
  166. project_key: str,
  167. ori_id: int,
  168. name: str,
  169. ip: str,
  170. rock: int,
  171. slot: int,
  172. port: int = 102,
  173. device_type: int = 1,
  174. tsap_conn_type: str | None = None,
  175. is_persistent: bool = False,
  176. device_group_id: int = 0,
  177. timeout: int = 3,
  178. alarm_interval: int = 90,
  179. collect_interval: int = 5,
  180. ) -> dict[str, Any]:
  181. return api_edit_s7_device(
  182. project_key,
  183. {
  184. "ori_id": ori_id,
  185. "name": name,
  186. "ip": ip,
  187. "rock": rock,
  188. "slot": slot,
  189. "port": port,
  190. "device_type": device_type,
  191. "tsap_conn_type": _resolve_collector_s7_tsap_conn_type(device_type, tsap_conn_type),
  192. "is_persistent": is_persistent,
  193. "device_group_id": device_group_id,
  194. "timeout": timeout,
  195. "alarm_interval": alarm_interval,
  196. "collect_interval": collect_interval,
  197. },
  198. )
  199. @mcp.tool(
  200. name="collector.s7_point_create",
  201. description=(
  202. "汇采-创建 S7 采集点位。调用 {data_collector_base_url}/api/collector/s7/point/add。"
  203. "创建点位必须传 payload.device_id、payload.name、payload.address、payload.data_type 或 payload.type,"
  204. "以及 payload.register_type 或 payload.register_area。payload 会补齐默认值: point_id='', "
  205. "scale_ratio=1, value_offset=0, group_Id=0, invalid_values='', valid_range_start=null, "
  206. "valid_range_end=null。register_type: 1=I输入区, 2=Q输出区, 3=M存储区, 4=DB数据块, "
  207. "5=V区, 6=AI模拟输入;register_area 可用 I、Q、M、DB、V、AI。"
  208. "data_type 可用 bool, uint8, int8, uint16, int16, uint32, int32, float32, float64。"
  209. "S7 地址格式:I/Q/M/V 布尔点通常为 byte.bit,如 10.2;DB 布尔点为 db.byte.bit,"
  210. "如 1.10.2;非布尔点 I/Q/M/V 通常为 byte 地址,如 10,DB 为 db.byte,如 1.10。"
  211. "响应透传上游 JSON,state=0 表示业务成功。"
  212. ),
  213. )
  214. def collector_s7_point_create(
  215. project_key: str,
  216. payload: dict[str, Any],
  217. ) -> dict[str, Any]:
  218. return api_create_s7_point(project_key, payload)
  219. @mcp.tool(
  220. name="collector.s7_point_edit",
  221. description=(
  222. "汇采-编辑 S7 采集点位。调用 {data_collector_base_url}/api/collector/s7/point/update。"
  223. "必须传 ori_id 原点位 id、device_id 所属设备 id、name、address、data_type,"
  224. "以及 register_type 或 register_area。ori_id 对应 collector.device_points 返回的 data.point[].id。"
  225. "该接口是全量更新语义,未传字段可能被默认值覆盖。默认参数: point_id='', "
  226. "scale_ratio=1, value_offset=0, group_Id=0, invalid_values='', valid_range_start=null, "
  227. "valid_range_end=null。register_type: 1=I, 2=Q, 3=M, 4=DB, 5=V, 6=AI。"
  228. "data_type 可用 bool, uint8, int8, uint16, int16, uint32, int32, float32, float64。"
  229. "响应透传上游 JSON,state=0 表示业务成功。"
  230. ),
  231. )
  232. def collector_s7_point_edit(
  233. project_key: str,
  234. ori_id: int,
  235. device_id: int,
  236. name: str,
  237. address: str,
  238. data_type: str,
  239. register_type: int = 0,
  240. register_area: str = "",
  241. point_id: str = "",
  242. scale_ratio: float = 1,
  243. value_offset: float = 0,
  244. group_id: int = 0,
  245. invalid_values: str = "",
  246. valid_range_start: float | None = None,
  247. valid_range_end: float | None = None,
  248. describe: str = "",
  249. ) -> dict[str, Any]:
  250. payload: dict[str, Any] = {
  251. "ori_id": ori_id,
  252. "device_id": device_id,
  253. "name": name,
  254. "address": address,
  255. "data_type": data_type,
  256. "point_id": point_id,
  257. "scale_ratio": scale_ratio,
  258. "value_offset": value_offset,
  259. "group_id": group_id,
  260. "invalid_values": invalid_values,
  261. "valid_range_start": valid_range_start,
  262. "valid_range_end": valid_range_end,
  263. "describe": describe,
  264. }
  265. if register_type:
  266. payload["register_type"] = register_type
  267. else:
  268. payload["register_area"] = register_area
  269. return api_edit_s7_point(project_key, payload)