modbus_server.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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_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 (
  11. modbus_point_collect_test as api_modbus_point_collect_test,
  12. modbus_raw_read as api_modbus_raw_read,
  13. )
  14. from .mcp_app import mcp
  15. JsonScalar = str | int | float | bool | None
  16. ProjectKeyParam = Annotated[str, Field(description="项目标识,来自 project.list 返回的 project_key。")]
  17. ModbusIpParam = Annotated[str, Field(description="Modbus TCP 设备地址。")]
  18. ModbusPortParam = Annotated[int, Field(description="Modbus TCP 端口。")]
  19. ModbusSlaveIdParam = Annotated[int, Field(description="Modbus 从站 ID。")]
  20. ModbusDeviceTypeParam = Annotated[str, Field(description="网关设备类型,默认 ModbusTCP。")]
  21. ModbusWordByteOrderParam = Annotated[str, Field(description="字节/字顺序,可选 ABCD、BADC、CDAB、DCBA。")]
  22. ModbusAddressBaseParam = Annotated[int, Field(description="地址基准或地址偏移,默认 0。")]
  23. ModbusFunctionCodeParam = Annotated[int, Field(description="功能码:1=线圈,2=离散输入,3=保持寄存器,4=输入寄存器。")]
  24. ModbusDataTypeParam = Annotated[
  25. str,
  26. Field(description="数据类型;支持 bool、int16、uint16、int32、uint32、int64、uint64、float32、float64 及常见别名。"),
  27. ]
  28. class FlexibleModel(BaseModel):
  29. model_config = ConfigDict(extra="allow")
  30. class ModbusDeviceCreateItem(BaseModel):
  31. name: str = Field(description="设备名称。")
  32. ip: str = Field(description="Modbus TCP/UDP 设备地址;RTU 设备可为空。")
  33. port: int = Field(description="Modbus TCP/UDP 端口;RTU 设备可为 0。")
  34. slave_id: int = Field(description="Modbus 从站 ID。")
  35. byte_order: int = Field(description="字节顺序:1=Big Endian,2=Small Endian。")
  36. word_order: int = Field(description="字顺序:1=Big Endian,2=Small Endian。")
  37. address_base: int = Field(description="地址基准;会转换为汇采接口 address_offset。")
  38. serial_port: str = Field(default="", description="RTU 串口名。")
  39. device_type: int = Field(default=1, description="协议类型:1=TCP,2=RTU,3=UDP,4=RTU OVER TCP,5=RTU OVER UDP。")
  40. timeout: int = Field(default=3, description="连接超时时间。")
  41. is_persistent: bool = Field(default=False, description="是否持久化连接。")
  42. baud_rate: int = Field(default=0, description="RTU 波特率。")
  43. data_bit: int = Field(default=0, description="RTU 数据位。")
  44. parity: int = Field(default=0, description="RTU 校验位。")
  45. stop_bit: int = Field(default=0, description="RTU 停止位。")
  46. mode: int = Field(default=0, description="连接模式。")
  47. retry_times: int = Field(default=0, description="重试次数。")
  48. group_id: int = Field(default=0, description="设备分组 id。")
  49. alarm_interval: int = Field(default=90, description="告警间隔,单位秒。")
  50. collect_interval: int = Field(default=5, description="采集周期,单位秒。")
  51. class ModbusPointCreateItem(BaseModel):
  52. device_id: int = Field(description="所属 Modbus 设备 id,来自 collector.device_list 或设备创建结果。")
  53. name: str = Field(description="点位名称。")
  54. address: int = Field(description="寄存器地址。")
  55. type: ModbusDataTypeParam
  56. func_code: int | None = Field(default=3, description="功能码或寄存器类型:1=Read Coils/线圈/0x,2=Read Discrete Inputs/离散输入/1x,3=Read Holding Registers/保持寄存器/4x,4=Read Input Registers/输入寄存器/3x。")
  57. register_type: str = Field(default="", description="寄存器类型;可用 coil、discrete_input、holding_register、input_register。")
  58. point_id: str = Field(default="", description="外部点位编码。")
  59. scale_ratio: float = Field(default=1, description="缩放系数。")
  60. value_offset: float = Field(default=0, description="值偏移。")
  61. group_id: int = Field(default=0, description="点位分组 id。")
  62. invalid_values: str = Field(default="", description="无效值列表,多个值用逗号分隔。")
  63. valid_range_start: float | None = Field(default=None, description="合法范围最小值。")
  64. valid_range_end: float | None = Field(default=None, description="合法范围最大值。")
  65. bit: int = Field(default=0, description="位索引;用于从寄存器中取指定 bit。")
  66. describe: str = Field(default="", description="点位描述。")
  67. class ModbusRawReadSpec(BaseModel):
  68. function_code: ModbusFunctionCodeParam
  69. address: int = Field(description="起始地址。")
  70. quantity: int = Field(description="读取数量,范围 1..125。")
  71. class ModbusPointCollectSpec(FlexibleModel):
  72. function_code: int | None = Field(default=None, description="功能码或寄存器类型:1=Read Coils/线圈/0x,2=Read Discrete Inputs/离散输入/1x,3=Read Holding Registers/保持寄存器/4x,4=Read Input Registers/输入寄存器/3x。")
  73. func_code: int | None = Field(default=None, description="功能码或寄存器类型:1=Read Coils/线圈/0x,2=Read Discrete Inputs/离散输入/1x,3=Read Holding Registers/保持寄存器/4x,4=Read Input Registers/输入寄存器/3x。")
  74. address: int = Field(description="寄存器地址。")
  75. type: ModbusDataTypeParam
  76. bit: int | None = Field(default=None, description="位索引;用于读取 bool 位。")
  77. name: str | None = Field(default=None, description="点位名称;测试读取时可不传。")
  78. class GatewayBaseOutput(FlexibleModel):
  79. code: int | str | None = Field(default=None, description="网关状态码;0 表示成功。")
  80. msg: str | None = Field(default=None, description="网关状态说明或错误信息。")
  81. class GatewayCommunicationData(FlexibleModel):
  82. communication: list[str] = Field(default_factory=list, description="本次请求期间捕获的通信报文或通信过程记录。")
  83. class ModbusRawReadOutput(GatewayBaseOutput):
  84. data: GatewayCommunicationData | None = Field(default=None, description="Modbus 原始读取结果。")
  85. class ModbusGatewayPoint(FlexibleModel):
  86. name: str | None = Field(default=None, description="点位名称。")
  87. address: int | None = Field(default=None, description="寄存器地址。")
  88. type: str | None = Field(default=None, description="数据类型。")
  89. value: JsonScalar = Field(default=None, description="转换后的点位值。")
  90. class ModbusPointCollectData(GatewayCommunicationData):
  91. points: list[ModbusGatewayPoint] = Field(default_factory=list, description="转换后的 Modbus 点位值列表。")
  92. class ModbusPointCollectOutput(GatewayBaseOutput):
  93. data: ModbusPointCollectData | None = Field(default=None, description="Modbus 点位读取结果。")
  94. class CollectorBaseOutput(FlexibleModel):
  95. state: int | str | None = Field(default=None, description="汇采状态码;0 表示成功。")
  96. state_info: str | None = Field(default=None, description="汇采状态说明。")
  97. data: Any = Field(default=None, description="汇采接口返回数据。")
  98. class CollectorPointEditOutput(FlexibleModel):
  99. state: int | str | None = Field(default=None, description="汇采状态码;0 表示成功。")
  100. state_info: str | None = Field(default=None, description="汇采状态说明。")
  101. class BatchError(FlexibleModel):
  102. index: int | None = Field(default=None, description="输入数组中的下标。")
  103. name: str | None = Field(default=None, description="输入项名称。")
  104. stage: str | None = Field(default=None, description="失败阶段。")
  105. error: str | None = Field(default=None, description="失败原因。")
  106. class DeviceCreateSummary(FlexibleModel):
  107. total: int | None = Field(default=None, description="输入设备总数。")
  108. created: int | None = Field(default=None, description="汇采创建设备成功数量。")
  109. matched: int | None = Field(default=None, description="创建后从设备列表匹配到设备 id 的数量。")
  110. failed: int | None = Field(default=None, description="失败数量。")
  111. class DeviceCreateResult(FlexibleModel):
  112. index: int | None = Field(default=None, description="输入 devices 数组中的下标。")
  113. name: str | None = Field(default=None, description="设备名称。")
  114. device_id: int | None = Field(default=None, description="匹配到的汇采设备 id。")
  115. create_response: dict[str, Any] | None = Field(default=None, description="汇采创建设备接口原始响应。")
  116. matched_device: dict[str, Any] | None = Field(default=None, description="设备列表中匹配到的设备详情。")
  117. class ModbusDeviceCreateOutput(FlexibleModel):
  118. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  119. summary: DeviceCreateSummary | None = Field(default=None, description="批量创建设备汇总。")
  120. results: list[DeviceCreateResult] = Field(default_factory=list, description="每个设备的创建和匹配结果。")
  121. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  122. class PointCreateSummary(FlexibleModel):
  123. total: int | None = Field(default=None, description="输入点位总数。")
  124. success: int | None = Field(default=None, description="汇采创建点位成功数量。")
  125. failed: int | None = Field(default=None, description="失败数量。")
  126. class PointCreateResult(FlexibleModel):
  127. index: int | None = Field(default=None, description="输入 points 数组中的下标。")
  128. name: str | None = Field(default=None, description="点位名称。")
  129. device_id: int | None = Field(default=None, description="所属 Modbus 设备 id。")
  130. response: dict[str, Any] | None = Field(default=None, description="汇采创建点位接口原始响应。")
  131. class ModbusPointCreateOutput(FlexibleModel):
  132. state: int | str | None = Field(default=None, description="批量创建状态;0 表示全部成功,1 表示存在失败。")
  133. summary: PointCreateSummary | None = Field(default=None, description="批量创建点位汇总。")
  134. results: list[PointCreateResult] = Field(default_factory=list, description="每个点位的创建结果。")
  135. errors: list[BatchError] = Field(default_factory=list, description="失败项列表。")
  136. @mcp.tool(
  137. name="modbus.raw_read",
  138. description=(
  139. "采集网关-通过 Modbus TCP 读取原始数据,只返回通信报文,不解析业务值。"
  140. "如需转换后的点位值,请使用 modbus.point_collect_test。"
  141. ),
  142. )
  143. def modbus_raw_read(
  144. project_key: ProjectKeyParam,
  145. ip: ModbusIpParam,
  146. port: ModbusPortParam,
  147. slave_id: ModbusSlaveIdParam,
  148. read: Annotated[ModbusRawReadSpec, Field(description="原始读取参数。")],
  149. device_type: ModbusDeviceTypeParam = "ModbusTCP",
  150. word_byte_order: ModbusWordByteOrderParam = "ABCD",
  151. address_base: ModbusAddressBaseParam = 0,
  152. ) -> ModbusRawReadOutput:
  153. return api_modbus_raw_read(
  154. project_key,
  155. ip=ip,
  156. port=port,
  157. slave_id=slave_id,
  158. read=_dump_model_or_dict(read),
  159. device_type=device_type,
  160. word_byte_order=word_byte_order,
  161. address_base=address_base,
  162. )
  163. @mcp.tool(
  164. name="modbus.point_collect_test",
  165. description=(
  166. "采集网关-读取 Modbus TCP 点位并转换为业务值。"
  167. ),
  168. )
  169. def modbus_point_collect_test(
  170. project_key: ProjectKeyParam,
  171. ip: ModbusIpParam,
  172. port: ModbusPortParam,
  173. slave_id: ModbusSlaveIdParam,
  174. points: Annotated[list[ModbusPointCollectSpec], Field(description="要读取的 Modbus 点位列表。")],
  175. device_type: ModbusDeviceTypeParam = "ModbusTCP",
  176. word_byte_order: ModbusWordByteOrderParam = "ABCD",
  177. address_base: ModbusAddressBaseParam = 0,
  178. ) -> ModbusPointCollectOutput:
  179. return api_modbus_point_collect_test(
  180. project_key,
  181. ip=ip,
  182. port=port,
  183. slave_id=slave_id,
  184. points=[_dump_model_or_dict(item) for item in points],
  185. device_type=device_type,
  186. word_byte_order=word_byte_order,
  187. address_base=address_base,
  188. )
  189. @mcp.tool(
  190. name="collector.modbus_device_create",
  191. description=(
  192. "汇采-批量创建 Modbus 设备。创建后会读取设备列表并匹配返回设备 id。"
  193. "word_byte_order 映射到 byte_order/word_order:ABCD=>1/1,BADC=>2/1,CDAB=>1/2,DCBA=>2/2。"
  194. ),
  195. )
  196. def collector_modbus_device_create(
  197. project_key: ProjectKeyParam,
  198. devices: Annotated[list[ModbusDeviceCreateItem], Field(description="要创建的 Modbus 设备列表。")],
  199. ) -> ModbusDeviceCreateOutput:
  200. return api_create_modbus_devices(project_key, [_dump_model_or_dict(item) for item in devices])
  201. @mcp.tool(
  202. name="collector.modbus_device_edit",
  203. description=(
  204. "汇采-编辑 Modbus 设备。编辑前设备不能处于已连接状态;"
  205. "若已连接,请先调用 collector.device_disconnect。"
  206. ),
  207. )
  208. def collector_modbus_device_edit(
  209. project_key: ProjectKeyParam,
  210. id: Annotated[int, Field(description="要编辑的原汇采设备 id。")],
  211. name: Annotated[str, Field(description="设备名称。")],
  212. device_type: Annotated[int, Field(description="协议类型:1=TCP,2=RTU,3=UDP,4=RTU OVER TCP,5=RTU OVER UDP。")],
  213. slave_id: ModbusSlaveIdParam,
  214. byte_order: Annotated[int, Field(description="字节顺序:1=Big Endian,2=Small Endian。")],
  215. word_order: Annotated[int, Field(description="字顺序:1=Big Endian,2=Small Endian。")],
  216. ip: Annotated[str, Field(description="Modbus TCP/UDP 设备地址;RTU 设备可为空。")]= "",
  217. port: Annotated[int, Field(description="Modbus TCP/UDP 端口;RTU 设备可为 0。")]= 0,
  218. serial_port: Annotated[str, Field(description="RTU 串口名。")]= "",
  219. timeout: Annotated[int, Field(description="连接超时时间。")]= 3,
  220. is_persistent: Annotated[bool, Field(description="是否持久化连接。")]= False,
  221. baud_rate: Annotated[int, Field(description="RTU 波特率。")]= 0,
  222. data_bit: Annotated[int, Field(description="RTU 数据位。")]= 0,
  223. parity: Annotated[int, Field(description="RTU 校验位。")]= 0,
  224. stop_bit: Annotated[int, Field(description="RTU 停止位。")]= 0,
  225. mode: Annotated[int, Field(description="连接模式。")]= 0,
  226. address_offset: Annotated[int, Field(description="地址偏移。")]= 0,
  227. retry_times: Annotated[int, Field(description="重试次数。")]= 0,
  228. device_group_id: Annotated[int, Field(description="设备分组 id。")]= 0,
  229. alarm_interval: Annotated[int, Field(description="告警间隔,单位秒。")]= 90,
  230. collect_interval: Annotated[int, Field(description="采集周期,单位秒。")]= 5,
  231. ) -> CollectorBaseOutput:
  232. return api_edit_modbus_device(
  233. project_key,
  234. {
  235. "ori_id": id,
  236. "name": name,
  237. "device_type": device_type,
  238. "ip": ip,
  239. "port": port,
  240. "slave_id": slave_id,
  241. "byte_order": byte_order,
  242. "word_order": word_order,
  243. "serial_port": serial_port,
  244. "timeout": timeout,
  245. "is_persistent": is_persistent,
  246. "baud_rate": baud_rate,
  247. "data_bit": data_bit,
  248. "parity": parity,
  249. "stop_bit": stop_bit,
  250. "mode": mode,
  251. "address_offset": address_offset,
  252. "retry_times": retry_times,
  253. "device_group_id": device_group_id,
  254. "alarm_interval": alarm_interval,
  255. "collect_interval": collect_interval,
  256. },
  257. )
  258. @mcp.tool(
  259. name="collector.modbus_point_create",
  260. description=(
  261. "汇采-批量创建 Modbus 采集点位。数据类型和寄存器类型会在调用汇采接口前规范化。"
  262. ),
  263. )
  264. def collector_modbus_point_create(
  265. project_key: ProjectKeyParam,
  266. points: Annotated[list[ModbusPointCreateItem], Field(description="要创建的 Modbus 采集点位列表。")],
  267. ) -> ModbusPointCreateOutput:
  268. return api_create_modbus_points(project_key, [_dump_model_or_dict(item) for item in points])
  269. def _dump_model_or_dict(item: Any) -> dict[str, Any]:
  270. if isinstance(item, BaseModel):
  271. return item.model_dump(exclude_none=True)
  272. return dict(item)
  273. @mcp.tool(
  274. name="collector.modbus_point_edit",
  275. description=(
  276. "汇采-编辑 Modbus 采集点位。数据类型和寄存器类型会在调用汇采接口前规范化。"
  277. ),
  278. )
  279. def collector_modbus_point_edit(
  280. project_key: ProjectKeyParam,
  281. id: Annotated[int, Field(description="要编辑的原采集点位 id,对应 collector.device_points 返回的点位 id。")],
  282. name: Annotated[str, Field(description="点位名称。")],
  283. address: Annotated[int, Field(description="寄存器地址。")],
  284. data_type: ModbusDataTypeParam,
  285. func_code: Annotated[int, Field(description="功能码或寄存器类型:1=Read Coils/线圈/0x,2=Read Discrete Inputs/离散输入/1x,3=Read Holding Registers/保持寄存器/4x,4=Read Input Registers/输入寄存器/3x。")],
  286. point_id: Annotated[str, Field(description="外部点位编码。")]= "",
  287. scale_ratio: Annotated[float, Field(description="缩放系数。")]= 1,
  288. value_offset: Annotated[float, Field(description="值偏移。")]= 0,
  289. group_id: Annotated[int, Field(description="点位分组 id。")]= 0,
  290. invalid_values: Annotated[str, Field(description="无效值列表,多个值用逗号分隔。")]= "",
  291. valid_range_start: Annotated[float | None, Field(description="合法范围最小值。")]= None,
  292. valid_range_end: Annotated[float | None, Field(description="合法范围最大值。")]= None,
  293. bit: Annotated[int, Field(description="位索引;用于从寄存器中取指定 bit。")]= 0,
  294. describe: Annotated[str, Field(description="点位描述。")]= "",
  295. ) -> CollectorPointEditOutput:
  296. payload: dict[str, Any] = {
  297. "ori_id": id,
  298. "name": name,
  299. "address": address,
  300. "type": data_type,
  301. "point_id": point_id,
  302. "scale_ratio": scale_ratio,
  303. "value_offset": value_offset,
  304. "group_id": group_id,
  305. "invalid_values": invalid_values,
  306. "valid_range_start": valid_range_start,
  307. "valid_range_end": valid_range_end,
  308. "bit": bit,
  309. "describe": describe,
  310. }
  311. payload["func_code"] = func_code
  312. return api_edit_modbus_point(project_key, payload)