s7_server.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. from __future__ import annotations
  2. from typing import Annotated, Any
  3. from pydantic import BaseModel, ConfigDict, Field
  4. from .collector_api import (
  5. create_s7_devices as api_create_s7_devices,
  6. create_s7_points as api_create_s7_points,
  7. edit_s7_device as api_edit_s7_device,
  8. edit_s7_point as api_edit_s7_point,
  9. )
  10. from .gateway_api import (
  11. s7_connect_scan as api_s7_connect_scan,
  12. s7_point_collect_test as api_s7_point_collect_test,
  13. s7_raw_read as api_s7_raw_read,
  14. )
  15. from .mcp_app import mcp
  16. JsonScalar = str | int | float | bool | None
  17. ProjectKeyParam = Annotated[str, Field(description="项目标识,来自 project.list 返回的 project_key。")]
  18. S7IpParam = Annotated[str, Field(description="PLC 地址。")]
  19. S7RockParam = Annotated[int, Field(description="机架号。")]
  20. S7SlotParam = Annotated[int, Field(description="槽号。")]
  21. S7PortParam = Annotated[int, Field(description="S7 TCP 端口,默认 102。")]
  22. S7DeviceTypeParam = Annotated[str, Field(description="网关设备类型,可用 S7-1200、S7-1500、S7-Smart200。")]
  23. S7TsapConnTypeParam = Annotated[str | None, Field(description="TSAP 连接类型,可用 PG、OP、BASIC;不传时默认 PG。")]
  24. S7DataTypeParam = Annotated[
  25. str,
  26. Field(description="数据类型;支持 bool、uint8、int8、uint16、int16、uint32、int32、float32、float64 及常见别名。"),
  27. ]
  28. class FlexibleModel(BaseModel):
  29. model_config = ConfigDict(extra="allow")
  30. class S7DeviceCreateItem(BaseModel):
  31. name: str = Field(description="设备名称。")
  32. ip: str = Field(description="PLC 地址。")
  33. rock: int = Field(description="机架号。")
  34. slot: int = Field(description="槽号。")
  35. port: int = Field(default=102, description="S7 TCP 端口。")
  36. device_type: int = Field(default=1, description="设备类型:1=S7-1200,2=S7-1500,3=S7-Smart200。")
  37. tsap_conn_type: str | None = Field(default=None, description="TSAP 连接类型,可用 PG、OP、BASIC;不传时默认 PG。")
  38. is_persistent: bool = Field(default=False, description="是否持久化连接。")
  39. device_group_id: int = Field(default=0, description="设备分组 id。")
  40. group_id: int | None = Field(default=None, description="设备分组 id;会转换为 device_group_id。")
  41. timeout: int = Field(default=3, description="连接超时时间。")
  42. alarm_interval: int = Field(default=90, description="告警间隔,单位秒。")
  43. collect_interval: int = Field(default=5, description="采集周期,单位秒。")
  44. class S7PointCreateItem(BaseModel):
  45. device_id: int = Field(description="所属 S7 设备 id,来自 collector.device_list 或设备创建结果。")
  46. name: str = Field(description="点位名称。")
  47. address: str = Field(description="S7 地址;DB 非 bool 通常为 db.byte,DB bool 为 db.byte.bit。")
  48. data_type: str | None = Field(default=None, description="数据类型;也可用 type 指定。")
  49. type: str | None = Field(default=None, description="数据类型别名;会转换为 data_type。")
  50. register_type: int = Field(description="寄存器区域:1=I,2=Q,3=M,4=DB,5=V,6=AI。")
  51. point_id: str = Field(default="", description="外部点位编码。")
  52. scale_ratio: float = Field(default=1, description="缩放系数。")
  53. value_offset: float = Field(default=0, description="值偏移。")
  54. group_id: int = Field(default=0, description="点位分组 id。")
  55. invalid_values: str = Field(default="", description="无效值列表,多个值用逗号分隔。")
  56. valid_range_start: float | None = Field(default=None, description="合法范围最小值。")
  57. valid_range_end: float | None = Field(default=None, description="合法范围最大值。")
  58. describe: str = Field(default="", description="点位描述。")
  59. class S7RawReadSpec(FlexibleModel):
  60. area: str = Field(description="读取区域,可用 DB、M、I、Q、V。")
  61. start: int = Field(description="起始字节地址。")
  62. size: int = Field(description="读取字节数。")
  63. db: int | None = Field(default=None, description="DB 块编号;area=DB 时使用。")
  64. class S7PointCollectSpec(FlexibleModel):
  65. area: str = Field(description="读取区域,可用 DB、M、I、Q、V。")
  66. start: int = Field(description="起始字节地址。")
  67. type: S7DataTypeParam
  68. db: int | None = Field(default=None, description="DB 块编号;area=DB 时使用。")
  69. bit: int | None = Field(default=None, description="bool 点读取的位索引,范围 0..7。")
  70. name: str | None = Field(default=None, description="点位名称;测试读取时可不传。")
  71. class GatewayBaseOutput(FlexibleModel):
  72. code: int | str | None = Field(default=None, description="网关状态码;0 表示成功。")
  73. msg: str | None = Field(default=None, description="网关状态说明或错误信息。")
  74. class S7CommunicationData(FlexibleModel):
  75. communication: list[str] = Field(default_factory=list, description="本次请求期间的连接、读取和断开过程记录。")
  76. class S7RawReadOutput(GatewayBaseOutput):
  77. data: S7CommunicationData | None = Field(default=None, description="S7 原始字节读取结果。")
  78. class S7GatewayPoint(FlexibleModel):
  79. name: str | None = Field(default=None, description="点位名称。")
  80. area: str | None = Field(default=None, description="读取区域。")
  81. start: int | None = Field(default=None, description="起始字节地址。")
  82. type: str | None = Field(default=None, description="数据类型。")
  83. value: JsonScalar = Field(default=None, description="转换后的点位值。")
  84. class S7PointCollectData(S7CommunicationData):
  85. points: list[S7GatewayPoint] = Field(default_factory=list, description="转换后的 S7 点位值列表。")
  86. class S7PointCollectOutput(GatewayBaseOutput):
  87. data: S7PointCollectData | None = Field(default=None, description="S7 点位读取结果。")
  88. class S7ConnectScanItem(FlexibleModel):
  89. rock: int | None = Field(default=None, description="可用机架号。")
  90. slot: int | None = Field(default=None, description="可用槽号。")
  91. tsap_conn_type: str | None = Field(default=None, description="可用 TSAP 连接类型。")
  92. device_type: str | None = Field(default=None, description="可用设备类型。")
  93. class S7ConnectScanData(FlexibleModel):
  94. available: list[S7ConnectScanItem] = Field(default_factory=list, description="扫描到的可用连接组合。")
  95. class S7ConnectScanOutput(GatewayBaseOutput):
  96. data: S7ConnectScanData | None = Field(default=None, description="S7 连接扫描结果。")
  97. class CollectorBaseOutput(FlexibleModel):
  98. state: int | str | None = Field(default=None, description="汇采状态码;0 表示成功。")
  99. state_info: str | None = Field(default=None, description="汇采状态说明。")
  100. data: Any = Field(default=None, description="汇采接口返回数据。")
  101. class CollectorPointEditOutput(FlexibleModel):
  102. state: int | str | None = Field(default=None, description="汇采状态码;0 表示成功。")
  103. state_info: str | None = Field(default=None, description="汇采状态说明。")
  104. class BatchError(FlexibleModel):
  105. index: int | None = Field(default=None, description="输入数组中的下标。")
  106. name: str | None = Field(default=None, description="输入项名称。")
  107. stage: str | None = Field(default=None, description="失败阶段。")
  108. error: str | None = Field(default=None, description="失败原因。")
  109. class DeviceCreateSummary(FlexibleModel):
  110. total: int | None = Field(default=None, description="输入设备总数。")
  111. created: int | None = Field(default=None, description="汇采创建设备成功数量。")
  112. matched: int | None = Field(default=None, description="创建后从设备列表匹配到设备 id 的数量。")
  113. failed: int | None = Field(default=None, description="失败数量。")
  114. class DeviceCreateResult(FlexibleModel):
  115. index: int | None = Field(default=None, description="输入 devices 数组中的下标。")
  116. name: str | None = Field(default=None, description="设备名称。")
  117. device_id: int | None = Field(default=None, description="匹配到的汇采设备 id。")
  118. create_response: dict[str, Any] | None = Field(default=None, description="汇采创建设备接口原始响应。")
  119. matched_device: dict[str, Any] | None = Field(default=None, description="设备列表中匹配到的设备详情。")
  120. class S7DeviceCreateOutput(FlexibleModel):
  121. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  122. summary: DeviceCreateSummary | None = Field(default=None, description="批量创建设备汇总。")
  123. results: list[DeviceCreateResult] = Field(default_factory=list, description="每个设备的创建和匹配结果。")
  124. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  125. class PointCreateSummary(FlexibleModel):
  126. total: int | None = Field(default=None, description="输入点位总数。")
  127. success: int | None = Field(default=None, description="汇采创建点位成功数量。")
  128. failed: int | None = Field(default=None, description="失败数量。")
  129. class PointCreateResult(FlexibleModel):
  130. index: int | None = Field(default=None, description="输入 points 数组中的下标。")
  131. name: str | None = Field(default=None, description="点位名称。")
  132. device_id: int | None = Field(default=None, description="所属 S7 设备 id。")
  133. response: dict[str, Any] | None = Field(default=None, description="汇采创建点位接口原始响应。")
  134. class S7PointCreateOutput(FlexibleModel):
  135. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  136. summary: PointCreateSummary | None = Field(default=None, description="批量创建点位汇总。")
  137. results: list[PointCreateResult] = Field(default_factory=list, description="每个点位的创建结果。")
  138. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  139. @mcp.tool(
  140. name="s7.raw_read",
  141. description=(
  142. "采集网关-对 Siemens S7 TCP 设备执行原始字节读取,不解析业务值。"
  143. ),
  144. )
  145. def s7_raw_read(
  146. project_key: ProjectKeyParam,
  147. ip: S7IpParam,
  148. read: Annotated[S7RawReadSpec, Field(description="原始字节读取参数。")],
  149. rock: S7RockParam = 0,
  150. slot: S7SlotParam = 1,
  151. device_type: S7DeviceTypeParam = "S7-1200",
  152. port: S7PortParam = 102,
  153. tsap_conn_type: S7TsapConnTypeParam = None,
  154. ) -> S7RawReadOutput:
  155. return api_s7_raw_read(
  156. project_key,
  157. ip=ip,
  158. rock=rock,
  159. slot=slot,
  160. read=_dump_model_or_dict(read),
  161. device_type=device_type,
  162. port=port,
  163. tsap_conn_type=tsap_conn_type,
  164. )
  165. @mcp.tool(
  166. name="s7.point_collect_test",
  167. description=(
  168. "采集网关-读取 Siemens S7 TCP 点位并转换为业务值。"
  169. ),
  170. )
  171. def s7_point_collect_test(
  172. project_key: ProjectKeyParam,
  173. ip: S7IpParam,
  174. rock: S7RockParam,
  175. slot: S7SlotParam,
  176. points: Annotated[list[S7PointCollectSpec], Field(description="要读取的 S7 点位列表。")],
  177. device_type: S7DeviceTypeParam = "S7-1200",
  178. port: S7PortParam = 102,
  179. tsap_conn_type: S7TsapConnTypeParam = None,
  180. ) -> S7PointCollectOutput:
  181. return api_s7_point_collect_test(
  182. project_key,
  183. ip=ip,
  184. rock=rock,
  185. slot=slot,
  186. points=[_dump_model_or_dict(item) for item in points],
  187. device_type=device_type,
  188. port=port,
  189. tsap_conn_type=tsap_conn_type,
  190. )
  191. @mcp.tool(
  192. name="s7.connect_scan",
  193. description=(
  194. "采集网关-扫描 Siemens S7 TCP 设备可用连接组合。"
  195. ),
  196. )
  197. def s7_connect_scan(project_key: ProjectKeyParam, ip: S7IpParam) -> S7ConnectScanOutput:
  198. return api_s7_connect_scan(project_key, ip=ip)
  199. def _resolve_collector_s7_tsap_conn_type(device_type: int, tsap_conn_type: str | None) -> str:
  200. normalized_tsap = str(tsap_conn_type or "").strip().upper()
  201. if normalized_tsap:
  202. return normalized_tsap
  203. return "PG"
  204. @mcp.tool(
  205. name="collector.s7_device_create",
  206. description=(
  207. "汇采-批量创建 S7 设备。创建后会读取设备列表并匹配返回设备 id。"
  208. ),
  209. )
  210. def collector_s7_device_create(
  211. project_key: ProjectKeyParam,
  212. devices: Annotated[list[S7DeviceCreateItem], Field(description="要创建的 S7 设备列表。")],
  213. ) -> S7DeviceCreateOutput:
  214. return api_create_s7_devices(project_key, [_dump_model_or_dict(item) for item in devices])
  215. @mcp.tool(
  216. name="collector.s7_device_edit",
  217. description=(
  218. "汇采-编辑 S7 设备。只有明确需要 OP/BASIC 时才显式传 tsap_conn_type。"
  219. "编辑前设备不能处于已连接状态;若已连接,请先调用 "
  220. "collector.device_disconnect,并传 device_type=s7。"
  221. ),
  222. )
  223. def collector_s7_device_edit(
  224. project_key: ProjectKeyParam,
  225. id: Annotated[int, Field(description="要编辑的原汇采设备 id。")],
  226. name: Annotated[str, Field(description="设备名称。")],
  227. ip: S7IpParam,
  228. rock: S7RockParam,
  229. slot: S7SlotParam,
  230. port: S7PortParam = 102,
  231. device_type: Annotated[int, Field(description="设备类型:1=S7-1200,2=S7-1500,3=S7-Smart200。")]= 1,
  232. tsap_conn_type: Annotated[str | None, Field(description="TSAP 连接类型,可用 PG、OP、BASIC;不传时默认 PG。")]= None,
  233. is_persistent: Annotated[bool, Field(description="是否持久化连接。")]= False,
  234. device_group_id: Annotated[int, Field(description="设备分组 id。")]= 0,
  235. timeout: Annotated[int, Field(description="连接超时时间。")]= 3,
  236. alarm_interval: Annotated[int, Field(description="告警间隔,单位秒。")]= 90,
  237. collect_interval: Annotated[int, Field(description="采集周期,单位秒。")]= 5,
  238. ) -> CollectorBaseOutput:
  239. return api_edit_s7_device(
  240. project_key,
  241. {
  242. "id": id,
  243. "name": name,
  244. "ip": ip,
  245. "rock": rock,
  246. "slot": slot,
  247. "port": port,
  248. "device_type": device_type,
  249. "tsap_conn_type": _resolve_collector_s7_tsap_conn_type(device_type, tsap_conn_type),
  250. "is_persistent": is_persistent,
  251. "device_group_id": device_group_id,
  252. "timeout": timeout,
  253. "alarm_interval": alarm_interval,
  254. "collect_interval": collect_interval,
  255. },
  256. )
  257. @mcp.tool(
  258. name="collector.s7_point_create",
  259. description=(
  260. "汇采-批量创建 S7 采集点位。数据类型会在调用汇采接口前规范化。"
  261. "address 地址格式:I/Q/M/V 布尔点通常为 byte.bit,如 10.2;DB 布尔点为 db.byte.bit,"
  262. "如 1.10.2;非布尔点 I/Q/M/V 通常为 byte 地址,如 10,DB 为 db.byte,如 1.10。"
  263. ),
  264. )
  265. def collector_s7_point_create(
  266. project_key: ProjectKeyParam,
  267. points: Annotated[list[S7PointCreateItem], Field(description="要创建的 S7 采集点位列表。")],
  268. ) -> S7PointCreateOutput:
  269. return api_create_s7_points(project_key, [_dump_model_or_dict(item) for item in points])
  270. def _dump_model_or_dict(item: Any) -> dict[str, Any]:
  271. if isinstance(item, BaseModel):
  272. return item.model_dump(exclude_none=True)
  273. return dict(item)
  274. @mcp.tool(
  275. name="collector.s7_point_edit",
  276. description=(
  277. "汇采-编辑 S7 采集点位。数据类型会在调用汇采接口前规范化。"
  278. ),
  279. )
  280. def collector_s7_point_edit(
  281. project_key: ProjectKeyParam,
  282. id: Annotated[int, Field(description="要编辑的原采集点位 id,对应 collector.device_points 返回的点位 id。")],
  283. device_id: Annotated[int, Field(description="所属 S7 设备 id。")],
  284. name: Annotated[str, Field(description="点位名称。")],
  285. address: Annotated[str, Field(description="S7 地址;DB 非 bool 通常为 db.byte,DB bool 为 db.byte.bit。")],
  286. data_type: S7DataTypeParam,
  287. register_type: Annotated[int, Field(description="寄存器区域:1=I,2=Q,3=M,4=DB,5=V,6=AI。")],
  288. point_id: Annotated[str, Field(description="外部点位编码。")]= "",
  289. scale_ratio: Annotated[float, Field(description="缩放系数。")]= 1,
  290. value_offset: Annotated[float, Field(description="值偏移。")]= 0,
  291. group_id: Annotated[int, Field(description="点位分组 id。")]= 0,
  292. invalid_values: Annotated[str, Field(description="无效值列表,多个值用逗号分隔。")]= "",
  293. valid_range_start: Annotated[float | None, Field(description="合法范围最小值。")]= None,
  294. valid_range_end: Annotated[float | None, Field(description="合法范围最大值。")]= None,
  295. describe: Annotated[str, Field(description="点位描述。")]= "",
  296. ) -> CollectorPointEditOutput:
  297. payload: dict[str, Any] = {
  298. "id": id,
  299. "device_id": device_id,
  300. "name": name,
  301. "address": address,
  302. "data_type": data_type,
  303. "point_id": point_id,
  304. "scale_ratio": scale_ratio,
  305. "value_offset": value_offset,
  306. "group_id": group_id,
  307. "invalid_values": invalid_values,
  308. "valid_range_start": valid_range_start,
  309. "valid_range_end": valid_range_end,
  310. "describe": describe,
  311. }
  312. payload["register_type"] = register_type
  313. return api_edit_s7_point(project_key, payload)