modbus_server.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. from __future__ import annotations
  2. from typing import Any, NotRequired, TypedDict
  3. from pydantic import BaseModel
  4. from .collector_api import (
  5. create_modbus_devices as api_create_modbus_devices,
  6. create_modbus_points as api_create_modbus_points,
  7. edit_modbus_device as api_edit_modbus_device,
  8. edit_modbus_point as api_edit_modbus_point,
  9. )
  10. from .gateway_api import modbus_point_collect_test as api_modbus_point_collect_test
  11. from .mcp_app import mcp
  12. class ModbusDeviceCreateItem(BaseModel):
  13. name: str
  14. device_type: int
  15. ip: str
  16. port: int
  17. slave_id: int
  18. byte_order: int
  19. word_order: int
  20. address_base: int
  21. serial_port: str = ""
  22. timeout: int = 3
  23. is_persistent: bool = False
  24. baud_rate: int = 0
  25. data_bit: int = 0
  26. parity: int = 0
  27. stop_bit: int = 0
  28. mode: int = 0
  29. retry_times: int = 0
  30. group_id: int = 0
  31. alarm_interval: int = 90
  32. collect_interval: int = 5
  33. class ModbusPointCreateItem(TypedDict):
  34. device_id: int
  35. name: str
  36. address: int
  37. type: str
  38. func_code: NotRequired[int]
  39. register_type: NotRequired[str]
  40. point_id: NotRequired[str]
  41. scale_ratio: NotRequired[float]
  42. value_offset: NotRequired[float]
  43. group_id: NotRequired[int]
  44. invalid_values: NotRequired[str]
  45. valid_range_start: NotRequired[float | None]
  46. valid_range_end: NotRequired[float | None]
  47. bit: NotRequired[int]
  48. describe: NotRequired[str]
  49. @mcp.tool(
  50. name="modbus.point_collect_test",
  51. description=(
  52. "通过采集网关读取 Modbus TCP 点位并转换为业务值。调用 "
  53. "{base_url}/api/dc-gateway/modbus/read_points。function_code: "
  54. "1=Read Coils/线圈,2=Read Discrete Inputs/离散输入,"
  55. "3=Read Holding Registers/保持寄存器,4=Read Input Registers/输入寄存器。"
  56. "word_byte_order 可选 ABCD、BADC、CDAB、DCBA。若读取成功后要创建汇采设备,"
  57. "映射为 byte_order/word_order: ABCD=>1/1, BADC=>2/1, CDAB=>1/2, DCBA=>2/2。"
  58. "响应透传上游 JSON,code=0 表示业务成功。"
  59. ),
  60. )
  61. def modbus_point_collect_test(
  62. project_key: str,
  63. ip: str,
  64. port: int,
  65. slave_id: int,
  66. points: list[dict[str, Any]],
  67. device_type: str = "ModbusTCP",
  68. word_byte_order: str = "ABCD",
  69. address_base: int = 0,
  70. ) -> dict[str, Any]:
  71. return api_modbus_point_collect_test(
  72. project_key,
  73. ip=ip,
  74. port=port,
  75. slave_id=slave_id,
  76. points=points,
  77. device_type=device_type,
  78. word_byte_order=word_byte_order,
  79. address_base=address_base,
  80. )
  81. @mcp.tool(
  82. name="collector.modbus_device_create",
  83. description=(
  84. "汇采-批量创建 Modbus 设备。依次调用 {data_collector_base_url}/api/collector/device,"
  85. "全部设备创建调用完成后会内部调用设备列表并匹配每个设备 id;如果匹配到多个设备,"
  86. "选择 id 最大的。devices 每项必须传 device_type 协议类型、ip IP 地址、"
  87. "port 端口号、name 名称、slave_id、word_order 字顺序、byte_order 字节顺序、"
  88. "address_base 地址基准。address_base 会转换为汇采接口的 address_offset。"
  89. "默认参数: type=modbus, timeout=3, is_persistent=false, group_id=0, "
  90. "alarm_interval=90, collect_interval=5, retry_times=0。"
  91. "注意 byte_order/word_order 是汇采枚举,不是网关 word_byte_order。"
  92. "byte_order: 1=Big Endian, 2=Small Endian。word_order: 1=Big Endian, 2=Small Endian。"
  93. "device_type: 1=TCP, 2=RTU, 3=UDP, 4=RTU OVER TCP, 5=RTU OVER UDP。"
  94. "采集网关 word_byte_order 映射: ABCD=>1/1, BADC=>2/1, CDAB=>1/2, DCBA=>2/2。"
  95. "返回批量结果,state=0 表示全部创建并匹配成功。"
  96. ),
  97. )
  98. def collector_modbus_device_create(
  99. project_key: str,
  100. devices: list[ModbusDeviceCreateItem],
  101. ) -> dict[str, Any]:
  102. return api_create_modbus_devices(project_key, [_dump_model_or_dict(item) for item in devices])
  103. @mcp.tool(
  104. name="collector.modbus_device_edit",
  105. description=(
  106. "汇采-编辑 Modbus 设备。调用 "
  107. "{data_collector_base_url}/api/collector/modbus/device/edit。"
  108. "这是 Modbus 专用旧编辑接口,必须传 ori_id 原设备 id、name、slave_id、"
  109. "word_order、byte_order、device_type 连接类型。TCP/UDP 类设备还必须传 ip 和 port;"
  110. "RTU 设备必须传 serial_port。"
  111. "编辑前设备不能处于已连接状态;若已连接,请先调用 collector.device_disconnect。"
  112. "该接口是全量更新语义,未传字段可能被默认值覆盖。"
  113. "默认参数: ip='', port=0, serial_port='', timeout=3, is_persistent=false, "
  114. "baud_rate=0, data_bit=0, parity=0, stop_bit=0, mode=0, address_offset=0, "
  115. "retry_times=0, device_group_id=0, alarm_interval=90, collect_interval=5。"
  116. "连接类型: 1=TCP, 2=RTU, 3=UDP, 4=RTU OVER TCP, 5=RTU OVER UDP。"
  117. "byte_order: 1=Big Endian, 2=Small Endian;word_order: 1=Big Endian, 2=Small Endian。"
  118. "响应透传上游 JSON,state=0 表示业务成功。"
  119. ),
  120. )
  121. def collector_modbus_device_edit(
  122. project_key: str,
  123. ori_id: int,
  124. name: str,
  125. device_type: int,
  126. slave_id: int,
  127. byte_order: int,
  128. word_order: int,
  129. ip: str = "",
  130. port: int = 0,
  131. serial_port: str = "",
  132. timeout: int = 3,
  133. is_persistent: bool = False,
  134. baud_rate: int = 0,
  135. data_bit: int = 0,
  136. parity: int = 0,
  137. stop_bit: int = 0,
  138. mode: int = 0,
  139. address_offset: int = 0,
  140. retry_times: int = 0,
  141. device_group_id: int = 0,
  142. alarm_interval: int = 90,
  143. collect_interval: int = 5,
  144. ) -> dict[str, Any]:
  145. return api_edit_modbus_device(
  146. project_key,
  147. {
  148. "ori_id": ori_id,
  149. "name": name,
  150. "device_type": device_type,
  151. "ip": ip,
  152. "port": port,
  153. "slave_id": slave_id,
  154. "byte_order": byte_order,
  155. "word_order": word_order,
  156. "serial_port": serial_port,
  157. "timeout": timeout,
  158. "is_persistent": is_persistent,
  159. "baud_rate": baud_rate,
  160. "data_bit": data_bit,
  161. "parity": parity,
  162. "stop_bit": stop_bit,
  163. "mode": mode,
  164. "address_offset": address_offset,
  165. "retry_times": retry_times,
  166. "device_group_id": device_group_id,
  167. "alarm_interval": alarm_interval,
  168. "collect_interval": collect_interval,
  169. },
  170. )
  171. @mcp.tool(
  172. name="collector.modbus_point_create",
  173. description=(
  174. "汇采-批量创建 Modbus 采集点位。依次调用 "
  175. "{data_collector_base_url}/api/collector/modbus/point/add_collect_point。"
  176. "points 每项必须传 device_id、name 名称、address 寄存器地址、"
  177. "type 数据类型,以及 func_code 或 register_type 寄存器类型。"
  178. "每个点位会补齐默认值: point_id='', scale_ratio=1, value_offset=0, group_id=0, "
  179. "invalid_values='', valid_range_start=null, valid_range_end=null, bit=0。"
  180. "func_code: 1=Read Coils/线圈,2=Read Discrete Inputs/离散输入,"
  181. "3=Read Holding Registers/保持寄存器,4=Read Input Registers/输入寄存器。"
  182. "register_type 可用 coil、discrete_input、holding_register、input_register。"
  183. "数据类型应使用汇采类型: bool, int16, uint16, int32, uint32, int64, uint64, float32, float64。"
  184. "常见点表类型映射: BOOL=>bool, SHORT=>int16, WORD=>uint16, LONG=>int32, "
  185. "DWORD=>uint32, FLOAT/REAL=>float32, DOUBLE=>float64, LONGLONG=>int64, QWORD=>uint64。"
  186. "响应透传上游 JSON,state=0 表示业务成功。"
  187. ),
  188. )
  189. def collector_modbus_point_create(
  190. project_key: str,
  191. points: list[ModbusPointCreateItem],
  192. ) -> dict[str, Any]:
  193. return api_create_modbus_points(project_key, points)
  194. def _dump_model_or_dict(item: Any) -> dict[str, Any]:
  195. if isinstance(item, BaseModel):
  196. return item.model_dump(exclude_none=True)
  197. return dict(item)
  198. @mcp.tool(
  199. name="collector.modbus_point_edit",
  200. description=(
  201. "汇采-编辑 Modbus 采集点位。调用 "
  202. "{data_collector_base_url}/api/collector/modbus/point/edit_collect_point。"
  203. "必须传 ori_id 原点位 id、name 名称、address 寄存器地址、data_type 数据类型,"
  204. "以及 func_code 或 register_type 寄存器类型。"
  205. "ori_id 对应 collector.device_points 返回的 data.point[].id。"
  206. "编辑点位不会迁移所属设备。"
  207. "该接口是全量更新语义,未传字段可能被默认值覆盖。"
  208. "默认参数: point_id='', scale_ratio=1, value_offset=0, group_id=0, "
  209. "invalid_values='', valid_range_start=null, valid_range_end=null, bit=0。"
  210. "func_code: 1=Read Coils/线圈,2=Read Discrete Inputs/离散输入,"
  211. "3=Read Holding Registers/保持寄存器,4=Read Input Registers/输入寄存器。"
  212. "register_type 可用 coil、discrete_input、holding_register、input_register。"
  213. "数据类型应使用汇采类型: bool, int16, uint16, int32, uint32, int64, uint64, float32, float64。"
  214. "常见点表类型映射: BOOL=>bool, SHORT=>int16, WORD=>uint16, LONG=>int32, "
  215. "DWORD=>uint32, FLOAT/REAL=>float32, DOUBLE=>float64, LONGLONG=>int64, QWORD=>uint64。"
  216. "响应透传上游 JSON,state=0 表示业务成功。"
  217. ),
  218. )
  219. def collector_modbus_point_edit(
  220. project_key: str,
  221. ori_id: int,
  222. name: str,
  223. address: int,
  224. data_type: str,
  225. func_code: int = 0,
  226. register_type: str = "",
  227. point_id: str = "",
  228. scale_ratio: float = 1,
  229. value_offset: float = 0,
  230. group_id: int = 0,
  231. invalid_values: str = "",
  232. valid_range_start: float | None = None,
  233. valid_range_end: float | None = None,
  234. bit: int = 0,
  235. describe: str = "",
  236. ) -> dict[str, Any]:
  237. payload: dict[str, Any] = {
  238. "ori_id": ori_id,
  239. "name": name,
  240. "address": address,
  241. "type": data_type,
  242. "point_id": point_id,
  243. "scale_ratio": scale_ratio,
  244. "value_offset": value_offset,
  245. "group_id": group_id,
  246. "invalid_values": invalid_values,
  247. "valid_range_start": valid_range_start,
  248. "valid_range_end": valid_range_end,
  249. "bit": bit,
  250. "describe": describe,
  251. }
  252. if func_code:
  253. payload["func_code"] = func_code
  254. else:
  255. payload["register_type"] = register_type
  256. return api_edit_modbus_point(project_key, payload)