├── .github └── workflows │ ├── docker-image-build.yml │ ├── docker-image-latest.yml │ ├── docker-image-v0.3.0.yml │ ├── docker-image-v0.4.5.yml │ ├── docker-image-v0.4.6.yml │ ├── docker-image-v0.4.8.yml │ ├── docker-image-v0.5.0.yml │ ├── docker-image-v0.5.2.yml │ └── docker-image-v0.5.3.yml ├── Dockerfile ├── LICENSE ├── MODEBUS.md ├── README-DEV.md ├── README.md ├── config.yaml ├── docker部署.md ├── form_config.json ├── form_voucher.json ├── form_voucher_type.json ├── global_data └── global_data.go ├── go.mod ├── go.sum ├── http_client └── client.go ├── http_server ├── rsp_message.go └── server.go ├── images ├── 协议插件.png └── 时序图.png ├── log_init.go ├── main.go ├── modbus-protocol-plugin.exe ├── modbus ├── master_command.go ├── modbus_gateway_device.go ├── modbus_sub_device.go ├── rtu_command.go ├── tcp_command.go └── utils.go ├── mqtt ├── mqtt_client.go ├── read_rtu_data.go └── read_tcp_data.go ├── services ├── handle_conn.go ├── read_rtu_data.go ├── read_tcp_data.go └── services.go ├── tp_config ├── command_raw.go └── sub_device_form_config.go ├── v3.0设计.md └── v3.0设计2.md /.github/workflows/docker-image-build.yml: -------------------------------------------------------------------------------- 1 | # 工作流名称 2 | name: Docker Image Build 3 | # 触发方式: 4 | # - Release触发: 当创建或更新 Release 时自动触发构建 5 | # - 手动触发: 可以通过 GitHub Actions 页面手动触发 6 | on: 7 | release: 8 | types: [published] 9 | workflow_dispatch: 10 | # 构建任务 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # 检出代码 16 | - name: 检出代码 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | # 获取版本号 21 | - name: 获取版本号 22 | id: get_version 23 | run: | 24 | VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo 'latest') 25 | echo "VERSION=$VERSION" >> $GITHUB_ENV 26 | # 添加仓库名小写转换 27 | echo "OWNER_LC=${GITHUB_REPOSITORY_OWNER,,}" >> $GITHUB_ENV 28 | # 登录各个镜像仓库 29 | - name: 登录镜像仓库 30 | run: | 31 | echo "${{ secrets.DOCKERHUB_TOKEN }}" | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin 32 | echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.repository_owner }} --password-stdin 33 | echo "${{ secrets.IMAGE_PASS }}" | docker login registry.cn-hangzhou.aliyuncs.com -u ${{ secrets.IMAGE_USER }} --password-stdin 34 | # 设置 Docker Buildx 35 | - name: 设置 Docker Buildx 36 | uses: docker/setup-buildx-action@v1 37 | # 构建并推送到 DockerHub 和 GitHub 38 | - name: 构建并推送到 GitHub/DockerHub 39 | uses: docker/build-push-action@v4 40 | with: 41 | context: . 42 | file: ./Dockerfile 43 | push: true 44 | tags: | 45 | thingspanel/modbus-protocol-plugin:${{ env.VERSION }} 46 | ghcr.io/${{ env.OWNER_LC }}/modbus-protocol-plugin:${{ env.VERSION }} 47 | # 推送到阿里云仓库 48 | - name: 推送到阿里云 49 | run: | 50 | docker pull ghcr.io/${{ env.OWNER_LC }}/modbus-protocol-plugin:${{ env.VERSION }} 51 | docker tag ghcr.io/${{ env.OWNER_LC }}/modbus-protocol-plugin:${{ env.VERSION }} registry.cn-hangzhou.aliyuncs.com/thingspanel/modbus-protocol-plugin:${{ env.VERSION }} 52 | docker push registry.cn-hangzhou.aliyuncs.com/thingspanel/modbus-protocol-plugin:${{ env.VERSION }} 53 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-latest.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-latest 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:latest 28 | 29 | - name: Login to Aliyuncs Docker Hub 30 | uses: docker/login-action@v2.2.0 31 | with: 32 | registry: registry.cn-hangzhou.aliyuncs.com 33 | username: ${{ secrets.IMAGE_USER }} 34 | password: ${{ secrets.IMAGE_PASS }} 35 | logout: false 36 | 37 | - name: Use Skopeo Tools Sync Image to Aliyuncs Docker Hub 38 | run: | 39 | skopeo copy docker://docker.io/thingspanel/modbus-protocol-plugin:latest docker://registry.cn-hangzhou.aliyuncs.com/thingspanel/modbus-protocol-plugin:latest 40 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.3.0.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.4.0 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.4.0 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.4.5.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.4.5 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.4.5 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.4.6.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.4.6 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.4.6 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.4.8.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.4.8 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.4.8 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.5.0.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.5.0 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.5.0 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.5.2.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.5.2 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.5.2 28 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-v0.5.3.yml: -------------------------------------------------------------------------------- 1 | name: docker-image-v0.5.3 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v3 12 | - name: Login to DockerHub 13 | uses: docker/login-action@v1 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v1 19 | 20 | - name: Build and push 21 | id: docker_build 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: ./Dockerfile 26 | push: true 27 | tags: thingspanel/modbus-protocol-plugin:v0.5.3 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | FROM golang:alpine 3 | WORKDIR $GOPATH/src/app 4 | ADD . ./ 5 | ENV GO111MODULE=on 6 | ENV GOPROXY="https://goproxy.io" 7 | ENV MODBUS_THINGSPANEL_ADDRESS=http://127.0.0.1:9999 8 | ENV MODBUS_MQTT_BROKER=127.0.0.1:1883 9 | ENV MODBUS_MQTT_QOS=0 10 | RUN go build 11 | EXPOSE 502 12 | EXPOSE 503 13 | RUN chmod +x modbus-protocol-plugin 14 | RUN pwd 15 | RUN ls -lrt 16 | ENTRYPOINT [ "./modbus-protocol-plugin" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 The Thingsboard Authors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /MODEBUS.md: -------------------------------------------------------------------------------- 1 | # MDOBUS 2 | 3 | ## 错误码 4 | 5 | 81功能码为异常,后面跟一个字节的错误代码 6 | 7 | | 代码 | 描述 | 8 | | -- | -- | 9 | | 01 | 不合法功能代码 从机接收的是一种不能执行功能代码。发出查询命令后,该代码指示无程序功能。 | 10 | | 02 | 不合法数据地址 接收的数据地址,是从机不允许的地址。 | 11 | | 03 | 不合法数据 查询数据区的值是从机不允许的值。 | 12 | | 04 | 从机设备故障 从机执行主机请求的动作时出现不可恢复的错误。 | 13 | | 05 | 确认 从机已接收请求处理数据,但需要较长 的处理时间,为避免主机出现超时错误而发送该确认响应。主机以此再发送一个“查询程序完成”未决定从机是否已完成处理。 | 14 | | 06 | 从机设备忙碌 从机正忙于处理一个长时程序命令,请 求主机在从机空闲时发送信息。 | 15 | | 07 | 否定 从机不能执行查询要求的程序功能时,该代码使用十进制 13 或 14 代码,向主机返回一个“不成功的编程请求”信息。主机应请求诊断从机的错误信息。 | 16 | | 08 | 内存奇偶校验错误 从机读扩展内存中的数据时,发现有奇偶校验错误,主机按从机的要求重新发送数据请求。 | -------------------------------------------------------------------------------- /README-DEV.md: -------------------------------------------------------------------------------- 1 | # 开发帮助 2 | 3 | ## SDK升级 4 | 5 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.0 6 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.2 7 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.3 8 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.4 9 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.5 10 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.6 11 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@v1.1.7 12 | go get -u github.com/ThingsPanel/tp-protocol-sdk-go@latest 13 | mosquitto_pub -h 47.115.210.16 -p 1883 -t "devices/telemetry" -m "{\"temp\":12.5}" -u "c55d8498" -P "c55d8498-e01e" -i "0" 14 | mosquitto_pub -h 47.115.210.16 -p 1883 -t "devices/telemetry" -m "{\"temp\":12.5}" -u "c55d8498" -P "c55d8498-e01e" -i "0" 15 | 16 | ## 测试 17 | 18 | 设备ID:7fa6bf8d-4803-d1a3-2c0c-84d1cee4b9ba 19 | pkg:xxxxxx 20 | 21 | 网关设备配置:MODBUS-TCP协议网关 22 | 子设备设备配置:MODBUS-TCP子设备配置 23 | 24 | ```json 25 | { 26 | "id": "7fa6bf8d-4803-d1a3-2c0c-84d1cee4b9ba", 27 | "voucher": "{\"reg_pkg\":\"xxxxxx\"}", 28 | "device_type": "2", 29 | "protocol_type": "MODBUS_TCP", 30 | "config": {}, 31 | "protocol_config_template": null, 32 | "sub_device": [ 33 | { 34 | "device_id": "046832de-4f6d-7708-4a87-1a429c7dd580", 35 | "voucher": "{\"default\":\"47cbe486-6565-4ec9-6020-7579820636e5\"}", 36 | "sub_device_addr": "xxxxxx", 37 | "config": {}, 38 | "protocol_config_template": { 39 | "CommandRawList": [ 40 | { 41 | "DataIdentifierListStr": "ewqe", 42 | "DataType": "coil", 43 | "DecimalPlacesListStr": "ewq", 44 | "Endianess": "LITTLE", 45 | "EquationListStr": "ewq", 46 | "FunctionCode": 1, 47 | "Interval": "ewq", 48 | "Quantity": "ewq", 49 | "StartingAddress": "ewq" 50 | }, 51 | { 52 | "DataIdentifierListStr": "fdsa", 53 | "DataType": "uint16", 54 | "DecimalPlacesListStr": "fdsa", 55 | "Endianess": "LITTLE", 56 | "EquationListStr": "fdsa", 57 | "FunctionCode": 2, 58 | "Interval": "fdsa", 59 | "Quantity": "fdsa", 60 | "StartingAddress": "fds" 61 | } 62 | ], 63 | "SlaveID": "wqewq" 64 | } 65 | } 66 | ] 67 | } 68 | ``` 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modbus Protocol Plugin 使用指南 2 | 3 | ## 简介 4 | 5 | ThingsPanel 支持通过开发协议插件服务来接入非 MQTT 协议的设备。本指南将介绍 Modbus Protocol Plugin 的部署和使用方法。 6 | 7 | ## 目录 8 | 9 | 1. [前置条件](#前置条件) 10 | 2. [部署步骤](#部署步骤) 11 | 3. [插件注册与配置](#插件注册与配置) 12 | 4. [系统架构](#系统架构) 13 | 5. [常见问题](#常见问题) 14 | 15 | ## 前置条件 16 | 17 | - Go 语言环境(版本 1.22.x 或更高) 18 | - Git 19 | - (可选)进程管理工具,如 PM2 20 | 21 | ## 部署步骤 22 | 23 | ### 1. 获取源码 24 | 25 | ```bash 26 | git clone https://github.com/ThingsPanel/modbus-protocol-plugin.git 27 | cd modbus-protocol-plugin 28 | ``` 29 | 30 | ### 2. 构建和运行 31 | 32 | 选择以下方法之一: 33 | 34 | #### 开发环境 35 | 36 | ```bash 37 | go run . start 38 | ``` 39 | 40 | #### 生产环境(推荐) 41 | 42 | ```bash 43 | go build -o modbus-plugin 44 | ./modbus-plugin start 45 | ``` 46 | 47 | ### 3. 使用进程管理工具(推荐) 48 | 49 | 使用 PM2 来提高可靠性和便于管理: 50 | 51 | ```bash 52 | # 安装 PM2(如果尚未安装) 53 | npm install -g pm2 54 | 55 | # 使用 PM2 启动应用 56 | pm2 start ./modbus-plugin --name "modbus-protocol-plugin" -- start 57 | 58 | # 设置开机自启 59 | pm2 startup 60 | pm2 save 61 | ``` 62 | 63 | ### 其他部署建议 64 | 65 | - 考虑使用 Docker 容器化应用,以简化部署和环境管理。 66 | 67 | ## 插件注册与配置 68 | 69 | 选择以下方法之一注册并配置插件: 70 | 71 | ### 方法一:手动注册和配置 72 | 73 | #### 步骤 1: 添加新插件 74 | 75 | 1. 登录超管用户 76 | 2. 导航至:应用管理 -> 插件管理 -> 添加新插件 77 | 3. 添加两个插件:MODBUS_TCP 和 MODBUS_RTU,填写以下信息: 78 | - 服务名称:必填,创建设备时会显示在选择协议下拉框中 79 | - 服务标识符:必填 80 | - 类别:必填 81 | - 版本:非必填 82 | 83 | 示例: 84 | 85 | | 服务名称 | 服务标识符 | 类别 | 版本 | 86 | |--------------|-----------|----------|--------| 87 | | MODBUS_TCP协议 | MODBUS_TCP | 接入协议 | v1.0.0 | 88 | | MODBUS_RTU协议 | MODBUS_RTU | 接入协议 | v1.0.0 | 89 | 90 | #### 步骤 2: 插件配置 91 | 92 | 添加完新插件后,点击"插件配置"进行详细设置: 93 | 94 | 1. HTTP服务地址:必填,插件HTTP服务的ip地址和端口(供平台后端和插件通讯) 95 | - 注意:如果MODBUS协议插件是Docker部署,这里要填平台后端能够访问到的ip 96 | 2. 设备类型:必填 97 | 3. 服务订阅主题前缀:必填 98 | 4. 设备接入地址:非必填,插件设备服务的ip地址和端口(仅作为平台中的提示信息,没有实际意义) 99 | 100 | 配置示例: 101 | 102 | | 服务名称 | HTTP服务地址 | 设备类型 | 服务订阅主题前缀 | 设备接入地址 | 103 | |--------------|--------------|----------|------------------|--------------------------| 104 | | MODBUS_TCP协议 | 127.0.0.1:503 | 网关设备 | plugin/modbus/ | [插件设备服务的ip地址]:502 | 105 | | MODBUS_RTU协议 | 127.0.0.1:503 | 网关设备 | plugin/modbus/ | [插件设备服务的ip地址]:502 | 106 | 107 | ### 方法二:SQL 导入 108 | 109 | (待完善) 110 | 111 | ## 系统架构 112 | 113 | ### 结构图 114 | 115 | ![结构图](./images/协议插件.png) 116 | 117 | ### 时序图 118 | 119 | ![时序图](images/时序图.png) 120 | 121 | ## 常见问题 122 | 123 | 如遇到安装或使用问题,可加入以下 QQ 群寻求帮助: 124 | 125 | - QQ 群①:260150504(已满) 126 | - QQ 群②:371794256 127 | 128 | 如需更多帮助或有特定部署需求,请联系 ThingsPanel 官方人员。 129 | -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | address: 0.0.0.0:502 #服务地址 3 | identifier1: MODBUS_RTU 4 | identifier2: MODBUS_TCP 5 | 6 | mqtt: 7 | broker: 127.0.0.1:1883 #mqtt服务端地址 8 | username: root 9 | password: root 10 | topic_to_publish_sub: devices/telemetry #订阅主题 11 | topic_to_publish: gateway/telemetry #发送主题 12 | topic_to_subscribe: plugin/modbus/# 13 | status_topic: device/status 14 | qos: 0 #qos 15 | 16 | http_server: 17 | address: 0.0.0.0:503 #http服务地址 18 | 19 | thingspanel: 20 | address: http://127.0.0.1:9999 #thingspanel服务地址 21 | 22 | log: 23 | # 日志级别 debug, info, warn, error, fatal, panic 24 | level: debug 25 | -------------------------------------------------------------------------------- /docker部署.md: -------------------------------------------------------------------------------- 1 | # docker部署 2 | 3 | ## 构建镜像 4 | 5 | ```bash 6 | docker build -t modbus-protocol-plugin:latest . 7 | ``` 8 | 9 | ## 如果平台是使用产品提供的docker-compose 部署的话,那么只需要在docker-compose.yml文件中添加以下内容即可: 10 | 11 | ```yml 12 | modbus_service: 13 | image: modbus-protocol-plugin:latest # 使用我们刚刚构建的镜像 14 | ports: 15 | - "502:502" 16 | - "503:503" 17 | environment: 18 | - "MODBUS_THINGSPANEL_ADDRESS=http://backend:9999" # 这里的地址是物联网平台的地址 19 | - "MODBUS_MQTT_BROKER=gmqtt:1883" # 这里的地址是mqtt的地址 20 | - "MODBUS_MQTT_QOS=0" 21 | networks: 22 | - thingspanel_network 23 | depends_on: 24 | - backend 25 | - gmqtt 26 | restart: unless-stopped 27 | ``` 28 | 29 | - 注意:如果您修改了 Docker Compose 文件,可能需要先运行以下命令(这会重新创建 modbus_service 容器而不重启其依赖的服务): 30 | 31 | ```bash 32 | docker-compose up -d --no-deps modbus_service 33 | ``` 34 | 35 | - 如果需要查看日志,可以运行: 36 | 37 | ```bash 38 | docker-compose logs -f modbus_service 39 | ``` 40 | -------------------------------------------------------------------------------- /form_config.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "type": "table", 4 | "label": "Modbus配置", 5 | "dataKey": "CommandRawList", 6 | "array": [ 7 | { 8 | "type": "select", 9 | "dataKey": "FunctionCode", 10 | "label": "功能码", 11 | "options": [ 12 | { 13 | "label": "01/05: 读/写线圈(开关量)", 14 | "value": 1 15 | }, 16 | { 17 | "label": "02: 读输入位状态(开关量只读)", 18 | "value": 2 19 | }, 20 | { 21 | "label": "03/06: 读/写保持寄存器(数值)", 22 | "value": 3 23 | }, 24 | { 25 | "label": "04: 读输入寄存器(数值只读)", 26 | "value": 4 27 | } 28 | ], 29 | "placeholder": "请选择要读取或写入的数据类型", 30 | "validate": { 31 | "type": "number", 32 | "required": true, 33 | "message": "请选择功能码" 34 | } 35 | }, 36 | { 37 | "type": "input", 38 | "dataKey": "Interval", 39 | "label": "采集周期(单位:秒)", 40 | "placeholder": "请输入数据采集的时间间隔", 41 | "validate": { 42 | "type": "number", 43 | "rules": "/^\\d{1,}$/", 44 | "required": true, 45 | "message": "采集周期必须为正整数" 46 | } 47 | }, 48 | { 49 | "type": "input", 50 | "dataKey": "StartingAddress", 51 | "label": "起始地址(十进制,从0开始)", 52 | "placeholder": "请输入寄存器或线圈的起始地址", 53 | "validate": { 54 | "type": "number", 55 | "rules": "/^\\d{1,}$/", 56 | "required": true, 57 | "message": "起始地址必须为非负整数" 58 | } 59 | }, 60 | { 61 | "type": "input", 62 | "dataKey": "Quantity", 63 | "label": "读取数量", 64 | "placeholder": "请输入要读取的连续地址数量(需与数据类型匹配)", 65 | "validate": { 66 | "type": "number", 67 | "rules": "/^\\d{1,}$/", 68 | "required": true, 69 | "message": "读取数量必须为正整数" 70 | } 71 | }, 72 | { 73 | "type": "select", 74 | "dataKey": "DataType", 75 | "label": "数据类型(功能码01/02选择线圈)", 76 | "options": [ 77 | { 78 | "label": "线圈(占用1个地址)", 79 | "value": "coil" 80 | }, 81 | { 82 | "label": "16位整数(占用1个地址)", 83 | "value": "int16" 84 | }, 85 | { 86 | "label": "16位无符号整数(占用1个地址)", 87 | "value": "uint16" 88 | }, 89 | { 90 | "label": "32位整数(占用2个地址)", 91 | "value": "int32" 92 | }, 93 | { 94 | "label": "32位无符号整数(占用2个地址)", 95 | "value": "uint32" 96 | }, 97 | { 98 | "label": "64位整数(占用4个地址)", 99 | "value": "int64" 100 | }, 101 | { 102 | "label": "32位浮点数(占用2个地址)", 103 | "value": "float32" 104 | }, 105 | { 106 | "label": "64位浮点数(占用4个地址)", 107 | "value": "float64" 108 | } 109 | ], 110 | "placeholder": "请选择数据存储格式", 111 | "validate": { 112 | "type": "string", 113 | "required": true, 114 | "message": "请选择数据类型" 115 | } 116 | }, 117 | { 118 | "type": "input", 119 | "dataKey": "DataIdentifierListStr", 120 | "label": "字段标识(多个字段用英文逗号分隔,数量需与读取数据的数量匹配,例如:temp1,temp2,humidity)", 121 | "placeholder": "请输入每个数据对应的字段名", 122 | "validate": { 123 | "type": "string", 124 | "required": true, 125 | "message": "字段标识不能为空" 126 | } 127 | }, 128 | { 129 | "type": "input", 130 | "dataKey": "EquationListStr", 131 | "label": "数值转换公式(多个字段的公式用英文逗号分隔,支持跨字段计算,例如:temp,temp1*10,temp2+temp1)", 132 | "placeholder": "可选,输入数值转换公式", 133 | "validate": { 134 | "type": "string", 135 | "required": false 136 | } 137 | }, 138 | { 139 | "type": "input", 140 | "dataKey": "DecimalPlacesListStr", 141 | "label": "小数位数(单个数字应用于所有字段,多个数字用逗号分隔对应各字段)", 142 | "placeholder": "可选,设置显示的小数位数", 143 | "validate": { 144 | "type": "string", 145 | "required": false 146 | } 147 | }, 148 | { 149 | "type": "select", 150 | "dataKey": "Endianess", 151 | "required": true, 152 | "label": "字节序", 153 | "options": [ 154 | { 155 | "label": "大端序(高字节在前)", 156 | "value": "BIG" 157 | }, 158 | { 159 | "label": "小端序(低字节在前)", 160 | "value": "LITTLE" 161 | } 162 | ], 163 | "placeholder": "请选择多字节数据的存储顺序", 164 | "validate": { 165 | "type": "string", 166 | "required": true, 167 | "message": "请选择字节序" 168 | } 169 | } 170 | ] 171 | } 172 | ] -------------------------------------------------------------------------------- /form_voucher.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "dataKey": "reg_pkg", 4 | "label": "注册包或心跳包(ASCII)", 5 | "placeholder": "please input the registration package", 6 | "type": "input", 7 | "validate": { 8 | "message": "The Registration Package cannot be empty", 9 | "required": true, 10 | "type": "string" 11 | } 12 | } 13 | ] -------------------------------------------------------------------------------- /form_voucher_type.json: -------------------------------------------------------------------------------- 1 | { 2 | "注册包(ASCII)":"REG_PKG" 3 | } -------------------------------------------------------------------------------- /global_data/global_data.go: -------------------------------------------------------------------------------- 1 | package globaldata 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/ThingsPanel/tp-protocol-sdk-go/api" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | // 平台网关配置map, key是网关的token,value是网关的配置 13 | // var GateWayConfigMap = make(map[string]*api.DeviceConfigResponseData) 14 | var GateWayConfigMap sync.Map 15 | 16 | var SubDeviceConfigMap sync.Map 17 | 18 | var SubDeviceIDAndGateWayIDMap sync.Map 19 | 20 | // 设备连接map, key是设备的token,value是设备的连接 21 | // var DeviceConnectionMap = make(map[string]*net.Conn) 22 | var DeviceConnectionMap sync.Map 23 | 24 | // 设备读写互斥锁 25 | var DeviceRWLock = map[string]*sync.Mutex{} 26 | 27 | // modbus错误码映射 28 | var ModbusErrorMap = map[byte]string{ 29 | 0x01: "Illegal function(非法功能)", 30 | 0x02: "Illegal data address(非法数据地址)", 31 | 0x03: "Illegal data value(非法数据值)", 32 | 0x04: "Slave device failure(从站设备故障)", 33 | 0x05: "Acknowledge(应答)", 34 | 0x06: "Slave device busy(从站设备忙)", 35 | 0x08: "Memory parity error(存储器奇偶校验错误)", 36 | 0x0A: "Gateway path unavailable(网关路径不可用)", 37 | 0x0B: "Gateway target device failed to respond(网关目标设备未响应)", 38 | } 39 | 40 | // modbus错误码方法,返回一个错误说明 41 | func GetModbusErrorDesc(code byte) string { 42 | if desc, ok := ModbusErrorMap[code]; ok { 43 | return desc 44 | } 45 | return "Unknown error:未知错误" 46 | } 47 | 48 | // 通过子设备ID获取网关配置 49 | func GetGateWayConfigByDeviceID(subDeviceID string) (*api.DeviceConfigResponseData, bool) { 50 | if gateWayID, ok := SubDeviceIDAndGateWayIDMap.Load(subDeviceID); ok { 51 | if gateWayConfig, ok := GateWayConfigMap.Load(gateWayID); ok { 52 | return gateWayConfig.(*api.DeviceConfigResponseData), true 53 | } else { 54 | logrus.Error("通过网关ID获取网关配置失败") 55 | return nil, false 56 | } 57 | } else { 58 | logrus.Error("通过子设备ID获取网关ID失败") 59 | return nil, false 60 | } 61 | } 62 | 63 | // 通过凭证获取regPkg,voucher{"reg_pkg":"` + regPkg + `"} 64 | func GetRegPkgByToken(voucher string) (string, bool) { 65 | // 去除可能存在的前缀和后缀空白字符 66 | voucher = strings.TrimSpace(voucher) 67 | 68 | // 检查 voucher 是否为空 69 | if voucher == "" { 70 | return "", false 71 | } 72 | 73 | // 定义一个结构体来解析 JSON 74 | var data struct { 75 | RegPkg string `json:"reg_pkg"` 76 | } 77 | 78 | // 解析 JSON 79 | err := json.Unmarshal([]byte(voucher), &data) 80 | if err != nil { 81 | return "", false 82 | } 83 | 84 | // 检查 RegPkg 是否为空 85 | if data.RegPkg == "" { 86 | return "", false 87 | } 88 | 89 | return data.RegPkg, true 90 | } 91 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ThingsPanel/modbus-protocol-plugin 2 | 3 | go 1.22 4 | 5 | require ( 6 | github.com/ThingsPanel/tp-protocol-sdk-go v1.1.8 7 | github.com/sirupsen/logrus v1.9.3 8 | ) 9 | 10 | require ( 11 | github.com/fsnotify/fsnotify v1.6.0 // indirect 12 | github.com/hashicorp/hcl v1.0.0 // indirect 13 | github.com/magiconair/properties v1.8.7 // indirect 14 | github.com/mitchellh/mapstructure v1.5.0 // indirect 15 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 16 | github.com/spf13/afero v1.9.5 // indirect 17 | github.com/spf13/cast v1.5.1 // indirect 18 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 19 | github.com/spf13/pflag v1.0.5 // indirect 20 | github.com/subosito/gotenv v1.4.2 // indirect 21 | golang.org/x/sys v0.22.0 // indirect 22 | golang.org/x/text v0.16.0 // indirect 23 | gopkg.in/ini.v1 v1.67.0 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | 27 | require ( 28 | github.com/Knetic/govaluate v3.0.0+incompatible 29 | github.com/eclipse/paho.mqtt.golang v1.4.3 30 | github.com/go-basic/uuid v1.0.0 // indirect 31 | github.com/gofrs/uuid v4.4.0+incompatible 32 | github.com/gorilla/websocket v1.5.3 // indirect 33 | github.com/spf13/viper v1.16.0 34 | golang.org/x/net v0.27.0 // indirect 35 | golang.org/x/sync v0.7.0 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= 4 | cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= 5 | cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 6 | cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= 7 | cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= 8 | cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= 9 | cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= 10 | cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= 11 | cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= 12 | cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= 13 | cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= 14 | cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= 15 | cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= 16 | cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= 17 | cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= 18 | cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= 19 | cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= 20 | cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= 21 | cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= 22 | cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 23 | cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= 24 | cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= 25 | cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= 26 | cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= 27 | cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= 28 | cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= 29 | cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= 30 | cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= 31 | cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= 32 | cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= 33 | cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= 34 | cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= 35 | cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= 36 | cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= 37 | cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= 38 | dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= 39 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 40 | github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 41 | github.com/Knetic/govaluate v3.0.0+incompatible h1:7o6+MAPhYTCF0+fdvoz1xDedhRb4f6s9Tn1Tt7/WTEg= 42 | github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= 43 | github.com/ThingsPanel/tp-protocol-sdk-go v1.1.8 h1:KBuT347SOaigtL5m1w1/dcaRmyeYga27xgwtC6ldQEk= 44 | github.com/ThingsPanel/tp-protocol-sdk-go v1.1.8/go.mod h1:88uDT+0yrKFn96AsT8ayxZUwSo/NRW14b6V/8zPOzQo= 45 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 46 | github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= 47 | github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= 48 | github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= 49 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 50 | github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= 51 | github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 52 | github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= 53 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 54 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 55 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 56 | github.com/eclipse/paho.mqtt.golang v1.4.3 h1:2kwcUGn8seMUfWndX0hGbvH8r7crgcJguQNCyp70xik= 57 | github.com/eclipse/paho.mqtt.golang v1.4.3/go.mod h1:CSYvoAlsMkhYOXh/oKyxa8EcBci6dVkLCbo5tTC1RIE= 58 | github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 59 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 60 | github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= 61 | github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= 62 | github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= 63 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 64 | github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= 65 | github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 66 | github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= 67 | github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 68 | github.com/go-basic/uuid v1.0.0 h1:Faqtetcr8uwOzR2qp8RSpkahQiv4+BnJhrpuXPOo63M= 69 | github.com/go-basic/uuid v1.0.0/go.mod h1:yVtVnsXcmaLc9F4Zw7hTV7R0+vtuQw00mdXi+F6tqco= 70 | github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= 71 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 72 | github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= 73 | github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1YrTJupqA= 74 | github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= 75 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 76 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 77 | github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 78 | github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 80 | github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 81 | github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= 82 | github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 83 | github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 84 | github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= 85 | github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= 86 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 89 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 90 | github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 91 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 92 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 93 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 94 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 95 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 96 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 97 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 98 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 99 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 100 | github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 101 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 102 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 103 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 104 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 105 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 106 | github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 107 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 108 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 109 | github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 110 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 111 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 112 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 113 | github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= 114 | github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 115 | github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= 116 | github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 117 | github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= 118 | github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 119 | github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 120 | github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 121 | github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 122 | github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= 123 | github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 124 | github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 125 | github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 126 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 127 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 128 | github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= 129 | github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= 130 | github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= 131 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= 132 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 133 | github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 134 | github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= 135 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= 136 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 137 | github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 138 | github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 139 | github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= 140 | github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= 141 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 142 | github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= 143 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 144 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 145 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 146 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 147 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 148 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 149 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 150 | github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= 151 | github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= 152 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 153 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 154 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 155 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 156 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 157 | github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= 158 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 159 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 160 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 161 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 162 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 163 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 164 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 165 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 166 | github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= 167 | github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= 168 | github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= 169 | github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= 170 | github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= 171 | github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= 172 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 173 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 174 | github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= 175 | github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= 176 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 177 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 178 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 179 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 180 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 181 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 182 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 183 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 184 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 185 | github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= 186 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 187 | github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= 188 | github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= 189 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 190 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 191 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 192 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 193 | go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= 194 | go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= 195 | go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 196 | go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 197 | go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= 198 | go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= 199 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 200 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 201 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 202 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 203 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 204 | golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= 205 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 206 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 207 | golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 208 | golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= 209 | golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= 210 | golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= 211 | golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 212 | golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 213 | golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= 214 | golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= 215 | golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= 216 | golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= 217 | golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= 218 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 219 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 220 | golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 221 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 222 | golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 223 | golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 224 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 225 | golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= 226 | golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 227 | golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 228 | golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= 229 | golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= 230 | golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= 231 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 232 | golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= 233 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 234 | golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 235 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 236 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 237 | golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 238 | golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 239 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 240 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 241 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 242 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 243 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 244 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 245 | golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 246 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 247 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 248 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 249 | golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 250 | golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 251 | golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 252 | golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 253 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 254 | golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 255 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 256 | golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 257 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 258 | golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 259 | golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 260 | golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 261 | golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 262 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 263 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 264 | golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 265 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 266 | golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 267 | golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 268 | golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 269 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 270 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 271 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 272 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 273 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 274 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 275 | golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 276 | golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 277 | golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 278 | golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 279 | golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 280 | golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 281 | golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= 282 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 283 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 284 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 285 | golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 286 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 287 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 288 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 289 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 290 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 291 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 292 | golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= 293 | golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 294 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 295 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 296 | golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 297 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 298 | golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 299 | golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 300 | golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 301 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 302 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 303 | golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 304 | golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 305 | golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 306 | golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 307 | golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 308 | golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 309 | golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 310 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 311 | golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 312 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 313 | golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 314 | golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 315 | golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 316 | golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 317 | golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 318 | golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 319 | golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 320 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 321 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 322 | golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 323 | golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 324 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 325 | golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 326 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 327 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 328 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 329 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 330 | golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 331 | golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= 332 | golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 333 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 334 | golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 335 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 336 | golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 337 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 338 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 339 | golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 340 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 341 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 342 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 343 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 344 | golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 345 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 346 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 347 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 348 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 349 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 350 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 351 | golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 352 | golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 353 | golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 354 | golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 355 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 356 | golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 357 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 358 | golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 359 | golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 360 | golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 361 | golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 362 | golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 363 | golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 364 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 365 | golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 366 | golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 367 | golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 368 | golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 369 | golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 370 | golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 371 | golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 372 | golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 373 | golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 374 | golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 375 | golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 376 | golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 377 | golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 378 | golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= 379 | golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 380 | golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 381 | golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 382 | golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 383 | golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 384 | golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 385 | golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 386 | golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 387 | golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= 388 | golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 389 | golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 390 | golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 391 | golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 392 | golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 393 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 394 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 395 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 396 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 397 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 398 | google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= 399 | google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= 400 | google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 401 | google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= 402 | google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 403 | google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 404 | google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= 405 | google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 406 | google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 407 | google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 408 | google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 409 | google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= 410 | google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 411 | google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= 412 | google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= 413 | google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= 414 | google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= 415 | google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= 416 | google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= 417 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 418 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 419 | google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 420 | google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= 421 | google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 422 | google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 423 | google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= 424 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 425 | google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 426 | google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 427 | google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 428 | google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= 429 | google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 430 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 431 | google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= 432 | google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 433 | google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 434 | google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 435 | google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 436 | google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 437 | google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 438 | google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= 439 | google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 440 | google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 441 | google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 442 | google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 443 | google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 444 | google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 445 | google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 446 | google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= 447 | google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= 448 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 449 | google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= 450 | google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 451 | google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 452 | google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 453 | google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 454 | google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 455 | google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 456 | google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 457 | google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 458 | google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 459 | google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= 460 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 461 | google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= 462 | google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 463 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 464 | google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= 465 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 466 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 467 | google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 468 | google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= 469 | google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= 470 | google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 471 | google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 472 | google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= 473 | google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= 474 | google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= 475 | google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= 476 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 477 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 478 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 479 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 480 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 481 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 482 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 483 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 484 | google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= 485 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 486 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 487 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 488 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 489 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 490 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 491 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 492 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 493 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 494 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 495 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 496 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 497 | honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 498 | honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 499 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 500 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 501 | honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 502 | honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= 503 | rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= 504 | rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= 505 | rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= 506 | -------------------------------------------------------------------------------- /http_client/client.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "time" 7 | 8 | tpprotocolsdkgo "github.com/ThingsPanel/tp-protocol-sdk-go" 9 | "github.com/ThingsPanel/tp-protocol-sdk-go/api" 10 | "github.com/sirupsen/logrus" 11 | "github.com/spf13/viper" 12 | ) 13 | 14 | var client *tpprotocolsdkgo.Client 15 | 16 | func Init() { 17 | addr := viper.GetString("thingspanel.address") 18 | logrus.Info("创建http客户端:", addr) 19 | client = tpprotocolsdkgo.NewClient(addr) 20 | go ServiceHeartbeat1() 21 | go ServiceHeartbeat2() 22 | } 23 | 24 | func GetDeviceConfig(voucher string, deviceID string) (*api.DeviceConfigResponse, error) { 25 | deviceConfigReq := api.DeviceConfigRequest{ 26 | Voucher: voucher, 27 | DeviceID: deviceID, 28 | } 29 | response, err := client.API.GetDeviceConfig(deviceConfigReq) 30 | if err != nil { 31 | errMsg := fmt.Sprintf("获取设备配置失败 (请求参数: %+v): %v", deviceConfigReq, err) 32 | logrus.Info(errMsg) 33 | return nil, fmt.Errorf(errMsg) 34 | } 35 | if response.Code != 200 { 36 | errMsg := fmt.Sprintf("获取设备配置失败 (请求参数: %+v): %v", deviceConfigReq, response.Message) 37 | return nil, fmt.Errorf(errMsg) 38 | } 39 | return response, nil 40 | } 41 | 42 | func ServiceHeartbeat1() { 43 | for { 44 | err := reportHeartbeat1() 45 | if err != nil { 46 | log.Println(err) 47 | } 48 | time.Sleep(50 * time.Second) 49 | } 50 | } 51 | 52 | // 这里需要改为自己的服务 53 | func reportHeartbeat1() error { 54 | sid := viper.GetString("server.identifier1") 55 | serviceHeartbeatReq := api.HeartbeatRequest{ 56 | ServiceIdentifier: sid, 57 | } 58 | response, err := client.API.Heartbeat(serviceHeartbeatReq) 59 | if err != nil { 60 | return fmt.Errorf("服务心跳上报失败 (请求参数:%+v): %v", serviceHeartbeatReq, err) 61 | } 62 | if response.Code != 200 { 63 | return fmt.Errorf("服务心跳上报失败 (请求参数:%+v): %v", serviceHeartbeatReq, response.Message) 64 | } 65 | return nil 66 | } 67 | func ServiceHeartbeat2() { 68 | for { 69 | err := reportHeartbeat2() 70 | if err != nil { 71 | log.Println(err) 72 | } 73 | time.Sleep(50 * time.Second) 74 | } 75 | } 76 | 77 | // 这里需要改为自己的服务 78 | func reportHeartbeat2() error { 79 | sid := viper.GetString("server.identifier2") 80 | serviceHeartbeatReq := api.HeartbeatRequest{ 81 | ServiceIdentifier: sid, 82 | } 83 | response, err := client.API.Heartbeat(serviceHeartbeatReq) 84 | if err != nil { 85 | return fmt.Errorf("服务心跳上报失败 (请求参数:%+v): %v", serviceHeartbeatReq, err) 86 | } 87 | if response.Code != 200 { 88 | return fmt.Errorf("服务心跳上报失败 (请求参数:%+v): %v", serviceHeartbeatReq, response.Message) 89 | } 90 | return nil 91 | } 92 | -------------------------------------------------------------------------------- /http_server/rsp_message.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | 8 | "github.com/sirupsen/logrus" 9 | ) 10 | 11 | // 返回错误信息 12 | func RspError(w http.ResponseWriter, err error) { 13 | var rspdata = make(map[string]interface{}) 14 | w.Header().Set("Content-Type", "application/json") 15 | rspdata["code"] = 400 16 | rspdata["message"] = err.Error() 17 | data, err := json.Marshal(rspdata) 18 | if err != nil { 19 | logrus.Info(err.Error()) 20 | } 21 | fmt.Fprint(w, string(data)) 22 | } 23 | 24 | // 返回成功信息 25 | func RspSuccess(w http.ResponseWriter, d interface{}) { 26 | var rspdata = make(map[string]interface{}) 27 | w.Header().Set("Content-Type", "application/json") 28 | rspdata["code"] = 200 29 | rspdata["message"] = "success" 30 | rspdata["data"] = d 31 | data, err := json.Marshal(rspdata) 32 | if err != nil { 33 | logrus.Info(err.Error()) 34 | } 35 | fmt.Fprint(w, string(data)) 36 | } 37 | -------------------------------------------------------------------------------- /http_server/server.go: -------------------------------------------------------------------------------- 1 | package httpserver 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "os" 10 | 11 | globaldata "github.com/ThingsPanel/modbus-protocol-plugin/global_data" 12 | service "github.com/ThingsPanel/modbus-protocol-plugin/services" 13 | tpprotocolsdkgo "github.com/ThingsPanel/tp-protocol-sdk-go" 14 | "github.com/sirupsen/logrus" 15 | "github.com/spf13/viper" 16 | ) 17 | 18 | var HttpClient *tpprotocolsdkgo.Client 19 | 20 | func Init() { 21 | go start() 22 | } 23 | 24 | func start() { 25 | var handler tpprotocolsdkgo.Handler = tpprotocolsdkgo.Handler{ 26 | // OnCreateDevice: OnCreateDevice, 27 | // OnUpdateDevice: OnUpdateDevice, 28 | // OnDeleteDevice: OnDeleteDevice, 29 | OnDisconnectDevice: OnDisconnectDevice, 30 | OnGetForm: OnGetForm, 31 | } 32 | addr := viper.GetString("http_server.address") 33 | logrus.Info("http服务启动:", addr) 34 | err := handler.ListenAndServe(addr) 35 | if err != nil { 36 | logrus.Info("ListenAndServe() failed, err: ", err) 37 | return 38 | } 39 | } 40 | 41 | // OnDisconnectDevice 断开设备 42 | func OnDisconnectDevice(w http.ResponseWriter, r *http.Request) { 43 | logrus.Info("OnDisconnectDevice") 44 | r.ParseForm() //解析参数,默认是不会解析的 45 | logrus.Info("【收到api请求】path", r.URL.Path) 46 | // 读取客户端发送的数据 47 | var reqDataMap = make(map[string]interface{}) 48 | if err := json.NewDecoder(r.Body).Decode(&reqDataMap); err != nil { 49 | r.Body.Close() 50 | w.WriteHeader(400) 51 | return 52 | } 53 | logrus.Info("reqDataMap:", reqDataMap) 54 | // 判断reqDataMap是否有key为device_id的值 55 | if _, ok := reqDataMap["device_id"]; !ok { 56 | RspError(w, errors.New("device_id not found")) 57 | return 58 | } 59 | // 判断reqDataMap["device_id"]的数据类型是否为string 60 | if _, ok := reqDataMap["device_id"].(string); !ok { 61 | RspError(w, errors.New("device_id type error")) 62 | return 63 | } 64 | deviceID := reqDataMap["device_id"].(string) 65 | err := disconnectDevice(deviceID) 66 | if err != nil { 67 | logrus.Error(err.Error()) 68 | r.Body.Close() 69 | w.WriteHeader(400) 70 | return 71 | } 72 | 73 | // 返回成功 74 | var rspdata = make(map[string]interface{}) 75 | w.Header().Set("Content-Type", "application/json") 76 | rspdata["code"] = 200 77 | rspdata["message"] = "success" 78 | data, err := json.Marshal(rspdata) 79 | if err != nil { 80 | logrus.Info(err.Error()) 81 | } 82 | fmt.Fprint(w, string(data)) 83 | } 84 | 85 | // OnGetForm 获取协议插件的json表单 86 | func OnGetForm(w http.ResponseWriter, r *http.Request) { 87 | logrus.Info("OnGetForm") 88 | r.ParseForm() //解析参数,默认是不会解析的 89 | logrus.Info("【收到api请求】path", r.URL.Path) 90 | logrus.Info("query", r.URL.Query()) 91 | 92 | device_type := r.URL.Query()["device_type"][0] 93 | form_type := r.URL.Query()["form_type"][0] 94 | protocol_type := r.URL.Query()["protocol_type"][0] 95 | // 如果请求参数protocol_type不等于MODBUS_RTU或MODBUS_TCP,返回空 96 | if protocol_type != "MODBUS_RTU" && protocol_type != "MODBUS_TCP" { 97 | RspError(w, errors.New("not support protocol type")) 98 | return 99 | } 100 | //CFG配置表单 VCR凭证表单 VCRT凭证类型表单 101 | switch form_type { 102 | case "CFG": 103 | if device_type == "3" { 104 | // 子设备配置表单 105 | RspSuccess(w, readFormConfigByPath("./form_config.json")) 106 | } else { 107 | RspSuccess(w, nil) 108 | } 109 | case "VCR": 110 | if device_type == "2" { 111 | // 网关凭证表单 112 | RspSuccess(w, readFormConfigByPath("./form_voucher.json")) 113 | } else { 114 | RspSuccess(w, nil) 115 | } 116 | case "VCRT": 117 | if device_type == "2" { 118 | // 网关凭证类型表单 119 | 120 | RspSuccess(w, readFormConfigByPath("./form_voucher_type.json")) 121 | } else { 122 | RspSuccess(w, nil) 123 | } 124 | default: 125 | RspError(w, errors.New("not support form type: "+form_type)) 126 | } 127 | } 128 | 129 | // 断开设备连接 130 | func disconnectDevice(deviceID string) error { 131 | // 获取连接 132 | conn, ok := globaldata.DeviceConnectionMap.Load(deviceID) 133 | if ok { 134 | c := *conn.(*net.Conn) 135 | // 如果本身是关闭的也无所谓,它会在读和写的时候返回错误 136 | service.CloseConnection(c, deviceID) 137 | } else { 138 | return errors.New("Connection not found for deviceID:" + deviceID) 139 | } 140 | return nil 141 | } 142 | 143 | // ./form_config.json 144 | func readFormConfigByPath(path string) interface{} { 145 | filePtr, err := os.Open(path) 146 | if err != nil { 147 | logrus.Info("文件打开失败...", err.Error()) 148 | return nil 149 | } 150 | defer filePtr.Close() 151 | var info interface{} 152 | // 创建json解码器 153 | decoder := json.NewDecoder(filePtr) 154 | err = decoder.Decode(&info) 155 | if err != nil { 156 | logrus.Info("解码失败", err.Error()) 157 | return info 158 | } else { 159 | logrus.Info("读取文件[form_config.json]成功...") 160 | return info 161 | } 162 | } 163 | 164 | // OnCreateDevice 创建设备 165 | // func OnCreateDevice(w http.ResponseWriter, r *http.Request) { 166 | // logrus.Info("OnCreateDevice") 167 | // r.ParseForm() //解析参数,默认是不会解析的 168 | // logrus.Info("【收到api请求】path", r.URL.Path) 169 | // logrus.Info("scheme", r.URL.Scheme) 170 | // // 读取客户端发送的数据 171 | // var reqDataMap = make(map[string]interface{}) 172 | // if err := json.NewDecoder(r.Body).Decode(&reqDataMap); err != nil { 173 | // r.Body.Close() 174 | // w.WriteHeader(400) 175 | // return 176 | // } 177 | 178 | // gateWayID := reqDataMap["ParentId"].(string) 179 | // err := updateGatewayConfig(gateWayID) 180 | // if err != nil { 181 | // logrus.Info(err.Error()) 182 | // r.Body.Close() 183 | // w.WriteHeader(400) 184 | // return 185 | // } 186 | // // 返回成功 187 | // var rspdata = make(map[string]interface{}) 188 | // w.Header().Set("Content-Type", "application/json") 189 | // rspdata["code"] = 200 190 | // rspdata["message"] = "success" 191 | // data, err := json.Marshal(rspdata) 192 | // if err != nil { 193 | // logrus.Info(err.Error()) 194 | // } 195 | // fmt.Fprint(w, string(data)) 196 | // } 197 | 198 | // OnUpdateDevice 更新设备 199 | // func OnUpdateDevice(w http.ResponseWriter, r *http.Request) { 200 | // logrus.Info("OnUpdateDevice") 201 | // r.ParseForm() //解析参数,默认是不会解析的 202 | // logrus.Info("【收到api请求】path", r.URL.Path) 203 | // logrus.Info("scheme", r.URL.Scheme) 204 | // // 读取客户端发送的数据 205 | // var reqDataMap = make(map[string]interface{}) 206 | // if err := json.NewDecoder(r.Body).Decode(&reqDataMap); err != nil { 207 | // r.Body.Close() 208 | // w.WriteHeader(400) 209 | // return 210 | // } 211 | 212 | // gateWayID := reqDataMap["ParentId"].(string) 213 | // err := updateGatewayConfig(gateWayID) 214 | // if err != nil { 215 | // logrus.Info(err.Error()) 216 | // r.Body.Close() 217 | // w.WriteHeader(400) 218 | // return 219 | // } 220 | // // 返回成功 221 | // var rspdata = make(map[string]interface{}) 222 | // w.Header().Set("Content-Type", "application/json") 223 | // rspdata["code"] = 200 224 | // rspdata["message"] = "success" 225 | // data, err := json.Marshal(rspdata) 226 | // if err != nil { 227 | // logrus.Info(err.Error()) 228 | // } 229 | // fmt.Fprint(w, string(data)) 230 | // } 231 | 232 | // OnDeleteDevice 删除设备 233 | // func OnDeleteDevice(w http.ResponseWriter, r *http.Request) { 234 | // logrus.Info("OnDeleteDevice") 235 | // r.ParseForm() //解析参数,默认是不会解析的 236 | // logrus.Info("【收到api请求】path", r.URL.Path) 237 | // logrus.Info("scheme", r.URL.Scheme) 238 | // // 读取客户端发送的数据 239 | // var reqDataMap = make(map[string]interface{}) 240 | // if err := json.NewDecoder(r.Body).Decode(&reqDataMap); err != nil { 241 | // r.Body.Close() 242 | // w.WriteHeader(400) 243 | // return 244 | // } 245 | // deviceType := reqDataMap["DeviceType"].(string) 246 | // // 子设备 247 | // if deviceType == "3" { 248 | // gateWayID := reqDataMap["ParentId"].(string) 249 | // err := updateGatewayConfig(gateWayID) 250 | // if err != nil { 251 | // logrus.Info(err.Error()) 252 | // r.Body.Close() 253 | // w.WriteHeader(400) 254 | // return 255 | // } 256 | // } 257 | // // 返回成功 258 | // var rspdata = make(map[string]interface{}) 259 | // w.Header().Set("Content-Type", "application/json") 260 | // rspdata["code"] = 200 261 | // rspdata["message"] = "success" 262 | // data, err := json.Marshal(rspdata) 263 | // if err != nil { 264 | // logrus.Info(err.Error()) 265 | // } 266 | // fmt.Fprint(w, string(data)) 267 | // } 268 | 269 | // 更新配置 270 | // func updateGatewayConfig(gateWayID string) error { 271 | // // 获取网关配置 272 | // gatewayConfig, err := httpclient.GetDeviceConfig("", gateWayID) 273 | // if err != nil { 274 | // return err 275 | // } 276 | // logrus.Info("网关配置:", gatewayConfig.Data) 277 | // // 获取连接 278 | // conn, ok := globaldata.DeviceConnectionMap.Load(gatewayConfig.Data.Voucher) 279 | // if ok { 280 | // c := *conn.(*net.Conn) 281 | // // 如果本身是关闭的也无所谓,它会在读和写的时候返回错误 282 | // service.CloseConnection(c, gatewayConfig.Data.Voucher) 283 | // } else { 284 | // return errors.New("Connection not found for token:" + gatewayConfig.Data.Voucher) 285 | // } 286 | // // 更换配置 287 | // globaldata.GateWayConfigMap.Store(gatewayConfig.Data.Voucher, &gatewayConfig.Data) 288 | // // 将设备连接存入全局变量 289 | // services.HandleConn(gatewayConfig.Data.Voucher, gatewayConfig.Data.ID) // 处理连接 290 | // return nil 291 | // } 292 | -------------------------------------------------------------------------------- /images/协议插件.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThingsPanel/modbus-protocol-plugin/23a5cf3d34a3193b69032281d6dcad4e49164f6d/images/协议插件.png -------------------------------------------------------------------------------- /images/时序图.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThingsPanel/modbus-protocol-plugin/23a5cf3d34a3193b69032281d6dcad4e49164f6d/images/时序图.png -------------------------------------------------------------------------------- /log_init.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "path" 6 | 7 | "github.com/sirupsen/logrus" 8 | "github.com/spf13/viper" 9 | ) 10 | 11 | type customFormatter struct { 12 | logrus.TextFormatter 13 | } 14 | 15 | func (f *customFormatter) Format(entry *logrus.Entry) ([]byte, error) { 16 | var levelColor string 17 | var levelText string 18 | switch entry.Level { 19 | case logrus.DebugLevel: 20 | levelColor, levelText = "34", "DEBUG" // 蓝色 21 | case logrus.InfoLevel: 22 | levelColor, levelText = "36", "INFO " // 青色 23 | case logrus.WarnLevel: 24 | levelColor, levelText = "33", "WARN " // 黄色 25 | case logrus.ErrorLevel: 26 | levelColor, levelText = "31", "ERROR" // 红色 27 | case logrus.FatalLevel, logrus.PanicLevel: 28 | levelColor, levelText = "31", "FATAL" // 红色,更重要的错误 29 | default: 30 | levelColor, levelText = "0", "UNKNOWN" // 默认颜色 31 | } 32 | 33 | // 获取调用者信息 34 | var fileAndLine string 35 | if entry.HasCaller() { 36 | fileAndLine = fmt.Sprintf("%s:%d", path.Base(entry.Caller.File), entry.Caller.Line) 37 | } 38 | 39 | // 组装格式化字符串 40 | msg := fmt.Sprintf("\033[1;%sm%s\033[0m \033[4;1;%sm[%s]\033[0m \033[1;%sm[%s]\033[0m %s\n", 41 | levelColor, levelText, // 日志级别,带颜色 42 | levelColor, entry.Time.Format("2006-01-02 15:04:05"), // 时间戳,下划线加颜色 43 | levelColor, fileAndLine, // 文件名:行号,带颜色 44 | entry.Message, // 日志消息 45 | ) 46 | 47 | return []byte(msg), nil 48 | } 49 | func LogInIt() { 50 | 51 | // 初始化 Logrus,不创建logrus实例,直接使用包级别的函数,这样可以在项目的任何地方使用logrus,目前不考虑多日志模块的情况 52 | logrus.SetReportCaller(true) 53 | logrus.SetFormatter(&customFormatter{logrus.TextFormatter{ 54 | ForceColors: true, 55 | FullTimestamp: true, 56 | }}) 57 | 58 | var logLevels = map[string]logrus.Level{ 59 | "panic": logrus.PanicLevel, 60 | "fatal": logrus.FatalLevel, 61 | "error": logrus.ErrorLevel, 62 | "warn": logrus.WarnLevel, 63 | "info": logrus.InfoLevel, 64 | "debug": logrus.DebugLevel, 65 | "trace": logrus.TraceLevel, 66 | } 67 | 68 | levelStr := viper.GetString("log.level") 69 | if level, ok := logLevels[levelStr]; ok { 70 | logrus.SetLevel(level) 71 | } else { 72 | logrus.Error("Invalid log level in config, setting to default level") 73 | logrus.SetLevel(logrus.InfoLevel) // 设置默认级别 74 | } 75 | 76 | logrus.Info("Logrus设置完成...") 77 | } 78 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "strings" 6 | 7 | //monitor "opcua/examples" 8 | httpclient "github.com/ThingsPanel/modbus-protocol-plugin/http_client" 9 | httpserver "github.com/ThingsPanel/modbus-protocol-plugin/http_server" 10 | mqtt "github.com/ThingsPanel/modbus-protocol-plugin/mqtt" 11 | deviceconfig "github.com/ThingsPanel/modbus-protocol-plugin/services" 12 | "github.com/spf13/viper" 13 | ) 14 | 15 | func main() { 16 | conf() 17 | log.Println("Starting the application...") 18 | LogInIt() 19 | // 启动mqtt客户端 20 | mqtt.InitClient() 21 | // 启动http客户端 22 | httpclient.Init() 23 | deviceconfig.Start() 24 | // 启动http服务 25 | httpserver.Init() 26 | // 订阅平台下发的消息 27 | mqtt.Subscribe() 28 | select {} 29 | } 30 | func conf() { 31 | log.Println("加载配置文件...") 32 | // 设置环境变量前缀 33 | viper.SetEnvPrefix("MODBUS") 34 | // 使 Viper 能够读取环境变量 35 | viper.AutomaticEnv() 36 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 37 | viper.SetConfigType("yaml") 38 | viper.SetConfigFile("./config.yaml") 39 | err := viper.ReadInConfig() 40 | if err != nil { 41 | log.Println(err.Error()) 42 | } 43 | log.Println("加载配置文件完成...") 44 | } 45 | -------------------------------------------------------------------------------- /modbus-protocol-plugin.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThingsPanel/modbus-protocol-plugin/23a5cf3d34a3193b69032281d6dcad4e49164f6d/modbus-protocol-plugin.exe -------------------------------------------------------------------------------- /modbus/master_command.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | type EndianessType string 13 | 14 | const ( 15 | BigEndian EndianessType = "BIG" 16 | LittleEndian EndianessType = "LITTLE" 17 | ) 18 | 19 | type MasterCommand struct { 20 | SlaveAddress byte // 从站地址 21 | FunctionCode byte // 功能码 22 | StartingAddress uint16 // 起始地址 23 | Quantity uint16 // 寄存器数量或数据数量 24 | ValueData []byte // 写入的数据 25 | Endianess EndianessType // 大端或小端 26 | Data []byte // 序列化后的数据 27 | } 28 | 29 | func NewCommand(requestType string, slaveAddress byte, functionCode byte, startingAddress uint16, quantity uint16, Endianess EndianessType) MasterCommand { 30 | return MasterCommand{ 31 | SlaveAddress: slaveAddress, 32 | FunctionCode: functionCode, 33 | StartingAddress: startingAddress, 34 | Quantity: quantity, 35 | Endianess: Endianess, // 大端或小端 36 | } 37 | } 38 | 39 | func (c *MasterCommand) Serialize() ([]byte, error) { 40 | var buf bytes.Buffer 41 | 42 | // 写入从站地址 43 | buf.WriteByte(c.SlaveAddress) 44 | logrus.Info("功能码:", c.FunctionCode) 45 | // 根据功能码进行序列化 46 | switch c.FunctionCode { 47 | case 0x01, 0x02, 0x03, 0x04: // Read Coils, Read Discrete Inputs, Read Holding Registers, Read Input Registers 48 | buf.WriteByte(c.FunctionCode) 49 | if c.Endianess == BigEndian { 50 | binary.Write(&buf, binary.BigEndian, c.StartingAddress) 51 | binary.Write(&buf, binary.BigEndian, c.Quantity) 52 | } else { 53 | binary.Write(&buf, binary.LittleEndian, c.StartingAddress) 54 | binary.Write(&buf, binary.LittleEndian, c.Quantity) 55 | } 56 | 57 | case 0x05: // Write Single Coil ValueData为空返回错误 58 | buf.WriteByte(c.FunctionCode) 59 | binary.Write(&buf, binary.BigEndian, c.StartingAddress) 60 | if bytes.Equal(c.ValueData, []byte{0xFF, 0x00}) { // 假设ValueData字段用于存储线圈的值 61 | binary.Write(&buf, binary.BigEndian, uint16(0xFF00)) 62 | } else if bytes.Equal(c.ValueData, []byte{0x00, 0x00}) { 63 | binary.Write(&buf, binary.BigEndian, uint16(0x0000)) 64 | } else { 65 | return nil, fmt.Errorf("ValueData is not empty: %s", hex.EncodeToString(c.ValueData)) 66 | } 67 | 68 | case 0x06: // Write Single Register ValueData为空返回错误 69 | if c.ValueData == nil { 70 | return nil, fmt.Errorf("ValueData is not empty") 71 | } 72 | buf.WriteByte(c.FunctionCode) 73 | 74 | if c.Endianess == BigEndian { 75 | binary.Write(&buf, binary.BigEndian, c.StartingAddress) 76 | binary.Write(&buf, binary.BigEndian, c.ValueData) 77 | } else { 78 | binary.Write(&buf, binary.LittleEndian, c.StartingAddress) 79 | binary.Write(&buf, binary.LittleEndian, c.ValueData) 80 | } 81 | 82 | // 我将这两个功能码合并,因为它们的序列化逻辑是相同的 83 | case 0x0F, 0x10: // Write Multiple Coils, Write Multiple Registers 84 | if c.ValueData == nil { 85 | return nil, fmt.Errorf("ValueData is not empty") 86 | } 87 | buf.WriteByte(c.FunctionCode) 88 | if c.Endianess == BigEndian { 89 | binary.Write(&buf, binary.BigEndian, c.StartingAddress) 90 | binary.Write(&buf, binary.BigEndian, c.ValueData) 91 | } else { 92 | binary.Write(&buf, binary.LittleEndian, c.StartingAddress) 93 | binary.Write(&buf, binary.LittleEndian, c.ValueData) 94 | } 95 | // 这里我们假设Data字段是[]byte类型并已经定义在Command结构中 96 | buf.WriteByte(byte(len(c.Data))) 97 | buf.Write(c.Data) 98 | 99 | default: 100 | return nil, fmt.Errorf("unsupported function code: %x", c.FunctionCode) 101 | } 102 | 103 | return buf.Bytes(), nil 104 | } 105 | -------------------------------------------------------------------------------- /modbus/modbus_gateway_device.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // type ModbusGateWayDevice struct { 4 | // GateWayDeviceToken string 5 | // SubDeviceMap map[string]*ModbusRTUSubDevice // SubDeviceMap是一个map,key是ModbusRTUSubDevice的子设备地址,value是ModbusRTUSubDevice 6 | // } 7 | -------------------------------------------------------------------------------- /modbus/modbus_sub_device.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | // type ModbusRTUSubDevice struct { 4 | // SlaveID uint8 5 | // RTUCommandMap map[string]*RTUCommand // RTUCommandMap是一个map,key是RTUCommand的ID,value是RTUCommand 6 | // } 7 | -------------------------------------------------------------------------------- /modbus/rtu_command.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/gofrs/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type RTUCommand struct { 14 | ID string // 唯一ID 15 | MasterCommand // 嵌入Command结构 16 | CRC uint16 // CRC校验值 17 | Data []byte 18 | // 为RTU特定的其他属性,如果有的话 19 | } 20 | 21 | func NewRTUCommand(slaveAddress byte, functionCode byte, startingAddress uint16, quantity uint16, endianess EndianessType) RTUCommand { 22 | // uuid.NewV4().String() 生成唯一ID 23 | id := uuid.Must(uuid.NewV4()).String() 24 | return RTUCommand{ 25 | ID: id, 26 | MasterCommand: MasterCommand{ 27 | SlaveAddress: slaveAddress, 28 | FunctionCode: functionCode, 29 | StartingAddress: startingAddress, 30 | Quantity: quantity, 31 | Endianess: endianess, 32 | }, 33 | } 34 | } 35 | 36 | // 序列化RTUCommand,会将CRC校验值附加到序列化后的数据后面并且赋值给RTUCommand.Data 37 | func (r *RTUCommand) Serialize() ([]byte, error) { 38 | // 使用MasterCommand的序列化方法 39 | data, err := r.MasterCommand.Serialize() 40 | if err != nil { 41 | return nil, err 42 | } 43 | // 计算CRC 44 | crcValue := crc16(data) 45 | 46 | // 创建一个新的buffer来包含序列化后的数据和CRC值 47 | var buf bytes.Buffer 48 | buf.Write(data) 49 | binary.Write(&buf, binary.LittleEndian, crcValue) 50 | 51 | // 将序列化后的结果赋值给r.Data 52 | r.Data = buf.Bytes() 53 | 54 | return buf.Bytes(), nil 55 | } 56 | 57 | // modbus返回的数据校验并去除CRC校验值 58 | func (r *RTUCommand) ParseAndValidateResponse(resp []byte) ([]byte, error) { 59 | if len(resp) < 3 { // minimal Modbus RTU frame size (1 addr + 1 function + 2 crc) 60 | return nil, errors.New("response too short") 61 | } 62 | if r.FunctionCode == 0x03 || r.FunctionCode == 0x04 || r.FunctionCode == 0x06 { 63 | //检查读取的数据与预设的数据长度不符合则丢弃 64 | if len(resp) != int(2*r.Quantity)+5 { 65 | return nil, fmt.Errorf("response length mismatch: expected %d but got %d", int(2*r.Quantity)+5, len(resp)) 66 | } 67 | } 68 | // CRC 校验值在 Modbus RTU 中始终是小端格式 69 | receivedCRC := binary.LittleEndian.Uint16(resp[len(resp)-2:]) 70 | 71 | // Compute CRC for the data without CRC 72 | computedCRC := crc16(resp[:len(resp)-2]) 73 | 74 | // Compare the received CRC with the computed CRC 75 | if receivedCRC != computedCRC { 76 | logrus.Infof("CRC mismatch: expected %04X but got %04X", computedCRC, receivedCRC) 77 | // return nil, fmt.Errorf("CRC mismatch: expected %04X but got %04X", computedCRC, receivedCRC) 78 | } 79 | 80 | return resp[:len(resp)-2], nil 81 | } 82 | -------------------------------------------------------------------------------- /modbus/tcp_command.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "fmt" 8 | 9 | "github.com/gofrs/uuid" 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type TCPCommand struct { 14 | ID string // 唯一ID 15 | MasterCommand // 嵌入Command结构 16 | Data []byte 17 | TransactionID uint16 // 事务标识符 18 | ProtocolID uint16 // 协议标识符, typically 0 for Modbus 19 | Length uint16 // 后续的字节数,包括单元标识符和数据 20 | } 21 | 22 | func NewTCPCommand(slaveAddress byte, functionCode byte, startingAddress uint16, quantity uint16, endianess EndianessType) TCPCommand { 23 | // uuid.NewV4().String() 生成唯一ID 24 | id := uuid.Must(uuid.NewV4()).String() 25 | return TCPCommand{ 26 | ID: id, 27 | MasterCommand: MasterCommand{ 28 | SlaveAddress: slaveAddress, 29 | FunctionCode: functionCode, 30 | StartingAddress: startingAddress, 31 | Quantity: quantity, 32 | Endianess: endianess, 33 | }, 34 | ProtocolID: 0, // Typically, this is 0 for Modbus 35 | } 36 | } 37 | 38 | // 序列化TCPCommand 39 | func (t *TCPCommand) Serialize() ([]byte, error) { 40 | // 使用MasterCommand的序列化方法 41 | data, err := t.MasterCommand.Serialize() 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | // 设置长度字段,长度字段是后续的字节数,包括单元标识符和数据 47 | t.Length = uint16(len(data)) 48 | 49 | // 创建一个新的buffer来包含TCP Modbus头和数据 50 | var buf bytes.Buffer 51 | binary.Write(&buf, binary.BigEndian, t.TransactionID) 52 | binary.Write(&buf, binary.BigEndian, t.ProtocolID) 53 | binary.Write(&buf, binary.BigEndian, t.Length) 54 | // buf.WriteByte(t.SlaveAddress) // 写入单元标识符 55 | buf.Write(data) 56 | 57 | // 将序列化后的结果赋值给t.Data 58 | t.Data = buf.Bytes() 59 | 60 | return buf.Bytes(), nil 61 | } 62 | 63 | const MBAPHeaderLength = 6 // MBAP Header length for Modbus TCP 64 | 65 | // 解析Modbus TCP返回的数据并提取数据部分 66 | func (r *TCPCommand) ParseTCPResponse(resp []byte) ([]byte, error) { 67 | if len(resp) < MBAPHeaderLength { // minimal Modbus TCP frame size 68 | logrus.Error("response too short") 69 | return nil, errors.New("response too short") 70 | } 71 | 72 | //检查读取的数据不等于数据长度则丢弃 73 | // if r.FunctionCode == 0x03 || r.FunctionCode == 0x04 || r.FunctionCode == 0x06 { 74 | // //检查读取的数据与预设的数据长度不符合则丢弃 75 | // if len(resp) != int(2*r.Quantity)+5 { 76 | // logrus.Error("response length mismatch") 77 | // return nil, fmt.Errorf("response length mismatch: expected %d but got %d", int(2*r.Quantity)+5, len(resp)) 78 | // } 79 | // } 80 | // Extracting MBAP fields from the response 81 | r.TransactionID = binary.BigEndian.Uint16(resp[0:2]) 82 | r.ProtocolID = binary.BigEndian.Uint16(resp[2:4]) 83 | r.Length = binary.BigEndian.Uint16(resp[4:6]) 84 | 85 | // Check if the ProtocolID is as expected (typically 0 for Modbus) 86 | if r.ProtocolID != 0 { 87 | logrus.Error("unexpected ProtocolID") 88 | return nil, fmt.Errorf("unexpected ProtocolID: %d", r.ProtocolID) 89 | } 90 | 91 | // Check if the length field in the MBAP header matches the actual response length 92 | if int(r.Length)+6 != len(resp) { // +6 because we don't include the 6 byte length of the MBAP header and 1 byte unit identifier in the length field of the MBAP header 93 | logrus.Error("length mismatch") 94 | return nil, fmt.Errorf("length mismatch: MBAP header reports %d bytes but received %d bytes", r.Length, len(resp)-MBAPHeaderLength) 95 | } 96 | 97 | // Extract and return the PDU (Protocol Data Unit) without the MBAP header 98 | return resp[MBAPHeaderLength:], nil 99 | } 100 | -------------------------------------------------------------------------------- /modbus/utils.go: -------------------------------------------------------------------------------- 1 | package modbus 2 | 3 | func crc16(data []byte) uint16 { 4 | const polynomial = 0xA001 5 | var crc = uint16(0xFFFF) 6 | 7 | for _, byteVal := range data { 8 | crc ^= uint16(byteVal) 9 | for i := 0; i < 8; i++ { 10 | if (crc & 0x0001) != 0 { 11 | crc = (crc >> 1) ^ polynomial 12 | } else { 13 | crc >>= 1 14 | } 15 | } 16 | } 17 | return crc 18 | } 19 | -------------------------------------------------------------------------------- /mqtt/mqtt_client.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net" 8 | "strings" 9 | "time" 10 | 11 | globaldata "github.com/ThingsPanel/modbus-protocol-plugin/global_data" 12 | "github.com/ThingsPanel/modbus-protocol-plugin/modbus" 13 | tpconfig "github.com/ThingsPanel/modbus-protocol-plugin/tp_config" 14 | "github.com/ThingsPanel/tp-protocol-sdk-go/api" 15 | "github.com/sirupsen/logrus" 16 | 17 | tpprotocolsdkgo "github.com/ThingsPanel/tp-protocol-sdk-go" 18 | MQTT "github.com/eclipse/paho.mqtt.golang" 19 | "github.com/spf13/viper" 20 | ) 21 | 22 | var MqttClient *tpprotocolsdkgo.MQTTClient 23 | 24 | func InitClient() { 25 | logrus.Info("创建mqtt客户端") 26 | // 创建新的MQTT客户端实例 27 | addr := viper.GetString("mqtt.broker") 28 | username := viper.GetString("mqtt.username") 29 | password := viper.GetString("mqtt.password") 30 | client := tpprotocolsdkgo.NewMQTTClient(addr, username, password) 31 | // 尝试连接到MQTT代理 32 | if err := client.Connect(); err != nil { 33 | log.Fatalf("连接失败: %v", err) 34 | } 35 | logrus.Info("连接成功") 36 | MqttClient = client 37 | } 38 | 39 | // 发布设备消息{"token":device_token,"values":{sub_device_addr1:{key:value...},sub_device_add2r:{key:value...}}} 40 | func Publish(payload string) error { 41 | // 主题 42 | topic := viper.GetString("mqtt.topic_to_publish_sub") 43 | qos := viper.GetUint("mqtt.qos") 44 | // 发布消息 45 | if err := MqttClient.Publish(topic, string(payload), uint8(qos)); err != nil { 46 | log.Printf("发布消息失败: %v", err) 47 | return err 48 | } 49 | logrus.Info("发布消息成功:", payload, "主题:", topic) 50 | return nil 51 | } 52 | 53 | // 订阅 54 | func Subscribe() { 55 | // 主题 56 | topic := viper.GetString("mqtt.topic_to_subscribe") 57 | qos := viper.GetUint("mqtt.qos") 58 | // 订阅主题 59 | if err := MqttClient.Subscribe(topic, messageHandler, uint8(qos)); err != nil { 60 | log.Printf("订阅主题失败: %v", err) 61 | } 62 | logrus.Info("订阅主题成功:", topic) 63 | 64 | } 65 | 66 | // 设备下发消息的回调函数:主题plugin/modbus/# payload:{sub_device_addr:{key:value...},sub_device_addr:{key:value...}} 67 | func messageHandler(client MQTT.Client, msg MQTT.Message) { 68 | logrus.Info("Received message on topic: ", msg.Topic()) 69 | logrus.Info("Received message: ", string(msg.Payload())) 70 | // 解析主题获取deviceID(plugin/modbus/devices/telemetry/control/# #为subDeviceID) 71 | subDeviceID := msg.Topic()[strings.LastIndex(msg.Topic(), "/")+1:] 72 | // 解析payload的json报文 73 | payloadMap := make(map[string]interface{}) 74 | if err := json.Unmarshal(msg.Payload(), &payloadMap); err != nil { 75 | logrus.Info(err) 76 | return 77 | } 78 | var subDevice *api.SubDevice 79 | if m, exists := globaldata.SubDeviceConfigMap.Load(subDeviceID); !exists { 80 | logrus.Info("子设备ID缓存中不存在") 81 | return 82 | } else { 83 | subDevice = m.(*api.SubDevice) 84 | } 85 | // 获取设备配置 86 | subDeviceFormConfig, err := tpconfig.NewSubDeviceFormConfig(subDevice.ProtocolConfigTemplate, subDevice.SubDeviceAddr) 87 | if err != nil { 88 | return 89 | } 90 | // 首先遍历dataMap 91 | for key, value := range payloadMap { 92 | // 遍历配置项 93 | for _, commandRaw := range subDeviceFormConfig.CommandRawList { 94 | // 遍历配置项的key 95 | for i, configKey := range strings.Split(commandRaw.DataIdetifierListStr, ",") { 96 | if key == strings.TrimSpace(configKey) { 97 | // 根据配置项的数据类型,将value转为对应的数据类型 98 | functionCode, startAddress, data, err := commandRaw.GetWriteCommand(key, value, i) 99 | if err != nil { 100 | logrus.Info(err) 101 | continue 102 | } 103 | //获取网关配置 104 | gateWayConfigMap, ok := globaldata.GetGateWayConfigByDeviceID(subDevice.DeviceID) 105 | if !ok { 106 | return 107 | } 108 | if gateWayConfigMap.ProtocolType == "MODBUS_RTU" { 109 | // 创建RTUCommand 110 | RTUCommand := modbus.NewRTUCommand(subDeviceFormConfig.SlaveID, functionCode, startAddress, 1, modbus.EndianessType(commandRaw.Endianess)) 111 | RTUCommand.ValueData = data 112 | 113 | sendData, err := RTUCommand.Serialize() 114 | if err != nil { 115 | logrus.Info(err) 116 | return 117 | } 118 | err = handleDeviceConnection(gateWayConfigMap.ID, sendData, gateWayConfigMap.Voucher, "MODBUS_RTU") 119 | if err != nil { 120 | logrus.Info(err) 121 | return 122 | } 123 | // 返回一次 124 | logrus.Info("控制成功,通知设备") 125 | err = PublishRsponse(key, value, subDevice.DeviceID) 126 | if err != nil { 127 | logrus.Info(err) 128 | } 129 | // 写完后需要再读一次 130 | // RTUCommand = modbus.NewRTUCommand(subDeviceFormConfig.SlaveID, commandRaw.FunctionCode, commandRaw.StartingAddress, commandRaw.Quantity, modbus.EndianessType(commandRaw.Endianess)) 131 | // regPkg, isTrue := globaldata.GetRegPkgByToken(gateWayConfigMap.Voucher) 132 | // if isTrue { 133 | // time.Sleep(2000 * time.Millisecond) 134 | // logrus.Debug("控制后再读一次") 135 | // //等待500毫秒 136 | // HandleRTUCommand(&RTUCommand, commandRaw, regPkg, subDevice, gateWayConfigMap.ID) 137 | // } 138 | 139 | // 反序列化数据 140 | } else if gateWayConfigMap.ProtocolType == "MODBUS_TCP" { 141 | // 创建TCPCommand 142 | TCPCommand := modbus.NewTCPCommand(subDeviceFormConfig.SlaveID, functionCode, startAddress, 1, modbus.EndianessType(commandRaw.Endianess)) 143 | TCPCommand.ValueData = data 144 | sendData, err := TCPCommand.Serialize() 145 | if err != nil { 146 | logrus.Info(err) 147 | return 148 | } 149 | err = handleDeviceConnection(gateWayConfigMap.ID, sendData, gateWayConfigMap.Voucher, "MODBUS_TCP") 150 | if err != nil { 151 | logrus.Info(err) 152 | return 153 | } 154 | // 返回一次 155 | logrus.Info("控制成功,通知设备") 156 | err = PublishRsponse(key, value, subDevice.DeviceID) 157 | if err != nil { 158 | logrus.Info(err) 159 | } 160 | } 161 | } 162 | } 163 | } 164 | 165 | } 166 | } 167 | 168 | // 处理设备连接 169 | func handleDeviceConnection(deviceID string, sendData []byte, voucher string, protocolType string) error { 170 | // 获取连接 171 | c, exists := globaldata.DeviceConnectionMap.Load(deviceID) 172 | if !exists { 173 | return fmt.Errorf("网关没有连接") 174 | } 175 | conn := *c.(*net.Conn) 176 | 177 | // 设置写超时时间 178 | err := conn.SetWriteDeadline(time.Now().Add(15 * time.Second)) 179 | if err != nil { 180 | logrus.Info("SetWriteDeadline() failed, err: ", err) 181 | return err 182 | } 183 | regPkg, isTrue := globaldata.GetRegPkgByToken(voucher) 184 | if isTrue { 185 | globaldata.DeviceRWLock[regPkg].Lock() 186 | logrus.Info("获取到锁:", regPkg) 187 | defer globaldata.DeviceRWLock[regPkg].Unlock() 188 | } 189 | logrus.Info("voucher:", voucher, "控制设备请求:", sendData) 190 | _, err = conn.Write(sendData) 191 | if err != nil { 192 | return fmt.Errorf("写入失败: %v", err) 193 | } 194 | 195 | // 读取数据 196 | // 设置读取超时时间 197 | err = conn.SetReadDeadline(time.Now().Add(15 * time.Second)) 198 | if err != nil { 199 | logrus.Info("SetReadDeadline() failed, err: ", err) 200 | return err 201 | } 202 | var buf []byte 203 | if protocolType == "MODBUS_RTU" { 204 | buf, err = ReadModbusRTUResponse(conn, regPkg) 205 | } else if protocolType == "MODBUS_TCP" { 206 | buf, err = ReadModbusTCPResponse(conn, regPkg) 207 | } 208 | if err != nil { 209 | return fmt.Errorf("读取失败: %v", err) 210 | } 211 | 212 | logrus.Info("voucher:", voucher, "控制设备响应:", buf) 213 | return nil 214 | } 215 | 216 | // 根据key、value组装发送 217 | func PublishRsponse(key string, value interface{}, subDeviceID string) error { 218 | dataMap := make(map[string]interface{}) 219 | dataMap[key] = value 220 | payloadMap := map[string]interface{}{ 221 | "device_id": subDeviceID, 222 | "values": dataMap, 223 | } 224 | var values []byte 225 | // 将payloadMap.values 转为json字符串 226 | values, err := json.Marshal(payloadMap["values"]) 227 | if err != nil { 228 | return err 229 | } 230 | logrus.Info("values:", string(values)) 231 | payloadMap["values"] = values 232 | payload, err := json.Marshal(payloadMap) 233 | if err != nil { 234 | return err 235 | } 236 | return Publish(string(payload)) 237 | } 238 | -------------------------------------------------------------------------------- /mqtt/read_rtu_data.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/hex" 6 | "fmt" 7 | "net" 8 | 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func isTimeout(err error) bool { 13 | if netErr, ok := err.(net.Error); ok { 14 | return netErr.Timeout() 15 | } 16 | return false 17 | } 18 | 19 | func ReadModbusRTUResponse(conn net.Conn, regPkg string) ([]byte, error) { 20 | var buffer bytes.Buffer 21 | readBuffer := make([]byte, 256) 22 | 23 | // 最多读取两次 24 | for i := 0; i < 2; i++ { 25 | n, err := conn.Read(readBuffer) 26 | logrus.Info("---------读取数据详情(第", i+1, "次)---------") 27 | logrus.Info("读取字节数: ", n) 28 | if n > 0 { 29 | logrus.Info("数据内容(hex): ", hex.EncodeToString(readBuffer[:n])) 30 | logrus.Info("数据内容(bytes): ", readBuffer[:n]) 31 | } 32 | if err != nil { 33 | logrus.Info("读取错误: ", err) 34 | logrus.Info("-----------------------------") 35 | if !isTimeout(err) { 36 | return nil, fmt.Errorf("连接异常: %v", err) 37 | } 38 | break 39 | } 40 | logrus.Info("-----------------------------") 41 | 42 | buffer.Write(readBuffer[:n]) 43 | 44 | // 尝试解析modbus响应 45 | if modbusData := findModbusResponse(buffer.Bytes()); modbusData != nil { 46 | return modbusData, nil 47 | } 48 | 49 | // 第一次没找到响应且没超时,继续读取 50 | if i == 0 && err == nil { 51 | continue 52 | } 53 | } 54 | 55 | return nil, nil 56 | } 57 | 58 | func findModbusResponse(data []byte) []byte { 59 | if len(data) < 5 { 60 | return nil 61 | } 62 | 63 | for i := 0; i < len(data)-4; i++ { 64 | if data[i] >= 0x30 && data[i] <= 0x39 { 65 | continue 66 | } 67 | 68 | if !isValidFunctionCode(data[i+1]) { 69 | continue 70 | } 71 | 72 | respLen, err := calculateResponseLength(data[i:]) 73 | if err != nil { 74 | continue 75 | } 76 | 77 | if i+respLen <= len(data) { 78 | return data[i : i+respLen] 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | func isValidFunctionCode(code byte) bool { 85 | validCodes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10} 86 | for _, valid := range validCodes { 87 | if code == valid { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | 94 | func calculateResponseLength(header []byte) (int, error) { 95 | if header[1]&0x80 != 0 { 96 | return 5, nil // 异常响应固定5字节 97 | } 98 | 99 | switch header[1] { 100 | case 0x01, 0x02: 101 | return int(header[2]) + 5, nil 102 | case 0x03, 0x04: 103 | return int(header[2]) + 5, nil 104 | case 0x05, 0x06, 0x0F, 0x10: 105 | return 8, nil 106 | default: 107 | return 0, fmt.Errorf("不支持的功能码: %02X", header[1]) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /mqtt/read_tcp_data.go: -------------------------------------------------------------------------------- 1 | package mqtt 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func ReadModbusTCPResponse(conn net.Conn, regPkg string) ([]byte, error) { 15 | var heartbeatResponse []byte 16 | 17 | // 第一个字节预读 18 | firstByte := make([]byte, 1) 19 | _, err := io.ReadFull(conn, firstByte) 20 | if err != nil { 21 | logrus.Warn("读取第一个字节失败:", err) 22 | return nil, fmt.Errorf("读取数据失败") 23 | } 24 | 25 | // 快速判断是否可能是心跳包(通过第一个字节特征) 26 | if regPkg != "" && firstByte[0] == regPkg[0] { 27 | // 可能是心跳包,继续读取剩余部分 28 | expectedBytes, _ := hex.DecodeString(regPkg) 29 | if len(expectedBytes) > 1 { 30 | remainingBytes := make([]byte, len(expectedBytes)-1) 31 | _, err = io.ReadFull(conn, remainingBytes) 32 | if err != nil { 33 | logrus.Warn("读取心跳包剩余数据失败:", err) 34 | return nil, fmt.Errorf("读取数据失败") 35 | } 36 | 37 | heartbeatResponse = append(firstByte, remainingBytes...) 38 | if bytes.Equal(heartbeatResponse, expectedBytes) { 39 | logrus.Debug("成功读取到心跳包响应") 40 | } 41 | } 42 | 43 | // 继续读取 Modbus 响应的第一个字节 44 | _, err = io.ReadFull(conn, firstByte) 45 | if err != nil { 46 | if heartbeatResponse != nil { 47 | // 如果已经读到心跳包,就返回心跳包 48 | return heartbeatResponse, nil 49 | } 50 | logrus.Warn("读取 Modbus 响应第一个字节失败:", err) 51 | return nil, fmt.Errorf("读取数据失败") 52 | } 53 | } 54 | 55 | // 读取剩余的 MBAP 头部(7字节) 56 | header := make([]byte, 7) 57 | _, err = io.ReadFull(conn, header) 58 | if err != nil { 59 | if heartbeatResponse != nil { 60 | // 如果已经读到心跳包,就返回心跳包 61 | return heartbeatResponse, nil 62 | } 63 | logrus.Warn("读取 Modbus TCP 报文头失败:", err) 64 | return nil, fmt.Errorf("读取报文头失败") 65 | } 66 | 67 | // 组合完整的头部(8字节) 68 | fullHeader := append(firstByte, header...) 69 | 70 | // 解析 MBAP 头 71 | length := binary.BigEndian.Uint16(fullHeader[4:6]) 72 | functionCode := fullHeader[7] 73 | 74 | // 计算需要读取的数据长度 75 | dataLength := int(length) - 2 // 减去单元ID和功能码长度 76 | if dataLength < 0 || dataLength > 256 { 77 | if heartbeatResponse != nil { 78 | // 如果已经读到心跳包,就返回心跳包 79 | return heartbeatResponse, nil 80 | } 81 | logrus.Warn("无效的数据长度") 82 | return nil, fmt.Errorf("无效的数据长度") 83 | } 84 | 85 | // 读取数据部分 86 | data := make([]byte, dataLength) 87 | _, err = io.ReadFull(conn, data) 88 | if err != nil { 89 | if heartbeatResponse != nil { 90 | // 如果已经读到心跳包,就返回心跳包 91 | return heartbeatResponse, nil 92 | } 93 | logrus.Warn("读取响应数据失败:", err) 94 | return nil, fmt.Errorf("读取响应数据失败") 95 | } 96 | 97 | // 组合完整响应 98 | modbusResponse := append(fullHeader, data...) 99 | logrus.Debugf("收到 Modbus 响应: 功能码=0x%02X, 数据长度=%d", functionCode, len(modbusResponse)) 100 | 101 | // 如果有心跳包响应,将其与 Modbus 响应组合 102 | if heartbeatResponse != nil { 103 | finalResponse := append(heartbeatResponse, modbusResponse...) 104 | return finalResponse, nil 105 | } 106 | 107 | return modbusResponse, nil 108 | } 109 | -------------------------------------------------------------------------------- /services/handle_conn.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "sync" 8 | "time" 9 | 10 | globaldata "github.com/ThingsPanel/modbus-protocol-plugin/global_data" 11 | "github.com/ThingsPanel/modbus-protocol-plugin/modbus" 12 | MQTT "github.com/ThingsPanel/modbus-protocol-plugin/mqtt" 13 | tpconfig "github.com/ThingsPanel/modbus-protocol-plugin/tp_config" 14 | "github.com/sirupsen/logrus" 15 | 16 | "github.com/ThingsPanel/tp-protocol-sdk-go/api" 17 | ) 18 | 19 | /* 20 | 说明: 21 | 硬件设备与平台连接后,一般不会变动配置 22 | 所以,不对子设备配置修改做局部更新 23 | 而是重新加载网关配置 24 | */ 25 | 26 | // HandleConn 处理单个连接 27 | func HandleConn(regPkg, deviceID string) { 28 | // 获取网关配置 29 | m, _ := globaldata.GateWayConfigMap.Load(deviceID) 30 | gatewayConfig := m.(*api.DeviceConfigResponseData) 31 | // 遍历网关的子设备 32 | for _, tpSubDevice := range gatewayConfig.SubDevices { 33 | // 存储子设备配置 34 | globaldata.SubDeviceConfigMap.Store(tpSubDevice.DeviceID, &tpSubDevice) 35 | // 存储子设备id和网关id的映射关系 36 | globaldata.SubDeviceIDAndGateWayIDMap.Store(tpSubDevice.DeviceID, deviceID) 37 | // 将tp子设备的表单配置转SubDeviceFormConfig 38 | subDeviceFormConfig, err := tpconfig.NewSubDeviceFormConfig(tpSubDevice.ProtocolConfigTemplate, tpSubDevice.SubDeviceAddr) 39 | if err != nil { 40 | logrus.Info(err.Error()) 41 | continue 42 | } 43 | // 遍历子设备的表单配置 44 | for _, commandRaw := range subDeviceFormConfig.CommandRawList { 45 | // 判断网关是ModbusRTU网关还是ModbusTCP网关 46 | var endianess modbus.EndianessType 47 | if gatewayConfig.ProtocolType == "MODBUS_RTU" { 48 | if commandRaw.Endianess == "BIG" { 49 | endianess = modbus.BigEndian 50 | } else if commandRaw.Endianess == "LITTLE" { 51 | endianess = modbus.LittleEndian 52 | } else { 53 | // 默认大端 54 | endianess = modbus.BigEndian 55 | } 56 | // 创建RTUCommand 57 | RTUCommand := modbus.NewRTUCommand(subDeviceFormConfig.SlaveID, commandRaw.FunctionCode, commandRaw.StartingAddress, commandRaw.Quantity, endianess) 58 | go handleRTUCommand(&RTUCommand, commandRaw, regPkg, &tpSubDevice, deviceID) 59 | 60 | } else if gatewayConfig.ProtocolType == "MODBUS_TCP" { 61 | if commandRaw.Endianess == "BIG" { 62 | endianess = modbus.BigEndian 63 | } else if commandRaw.Endianess == "LITTLE" { 64 | endianess = modbus.LittleEndian 65 | } else { 66 | // 默认大端 67 | endianess = modbus.BigEndian 68 | } 69 | // 创建TCPCommand 70 | TCPCommand := modbus.NewTCPCommand(subDeviceFormConfig.SlaveID, commandRaw.FunctionCode, commandRaw.StartingAddress, commandRaw.Quantity, endianess) 71 | go handleTCPCommand(&TCPCommand, commandRaw, regPkg, &tpSubDevice, deviceID) 72 | } 73 | } 74 | } 75 | } 76 | 77 | // 开启线程处理RTUCommand 78 | func handleRTUCommand(RTUCommand *modbus.RTUCommand, commandRaw *tpconfig.CommandRaw, regPkg string, tpSubDevice *api.SubDevice, deviceID string) { 79 | data, err := RTUCommand.Serialize() 80 | if err != nil { 81 | logrus.Info(err.Error()) 82 | return 83 | } 84 | 85 | m, exists := globaldata.DeviceConnectionMap.Load(deviceID) 86 | if !exists { 87 | logrus.Info("No connection found for regPkg:", regPkg, " deviceID:", deviceID) 88 | return 89 | } 90 | gatewayConn := m.(*net.Conn) 91 | conn := *gatewayConn 92 | defer CloseConnection(conn, deviceID) 93 | 94 | for { 95 | isClose, err := sendRTUDataAndProcessResponse(conn, data, RTUCommand, commandRaw, regPkg, tpSubDevice) 96 | if err != nil { 97 | logrus.Info("Error processing data:", err.Error()) 98 | if isClose { 99 | conn.Close() 100 | return 101 | } 102 | 103 | } 104 | // 间隔时间不能小于1秒 105 | if commandRaw.Interval < 1 { 106 | commandRaw.Interval = 1 107 | } 108 | time.Sleep(time.Duration(commandRaw.Interval) * time.Second) 109 | } 110 | } 111 | 112 | // clearBuffer 清空连接缓冲区 113 | func clearBuffer(conn net.Conn) error { 114 | buf := make([]byte, 1024) 115 | for { 116 | // 设置读取超时时间为100ms 117 | err := conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | _, err = conn.Read(buf) 123 | if err != nil { 124 | // 如果是超时错误,说明缓冲区已清空 125 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 126 | return nil 127 | } 128 | return err 129 | } 130 | } 131 | } 132 | 133 | func sendDataAndReadResponse(conn net.Conn, data []byte, regPkg string, modbusType string) (int, []byte, error) { 134 | // 获取锁 135 | if _, exists := globaldata.DeviceRWLock[regPkg]; !exists { 136 | globaldata.DeviceRWLock[regPkg] = &sync.Mutex{} 137 | } 138 | globaldata.DeviceRWLock[regPkg].Lock() 139 | logrus.Info("获取到锁:", regPkg) 140 | defer globaldata.DeviceRWLock[regPkg].Unlock() 141 | logrus.Info("regPkg:", regPkg, " 请求:", data) 142 | 143 | // 清空缓冲区 144 | if err := clearBuffer(conn); err != nil { 145 | logrus.Info("清空缓冲区失败:", err) 146 | } 147 | 148 | // 设置写超时时间 149 | err := conn.SetWriteDeadline(time.Now().Add(1 * time.Second)) 150 | if err != nil { 151 | logrus.Info("SetWriteDeadline() failed, err: ", err) 152 | } 153 | 154 | // 写入数据 155 | _, err = conn.Write(data) 156 | if err != nil { 157 | return 0, nil, err 158 | } 159 | 160 | // 设置读取超时时间 161 | err = conn.SetReadDeadline(time.Now().Add(1 * time.Second)) 162 | if err != nil { 163 | logrus.Info("SetReadDeadline() failed, err: ", err) 164 | } 165 | 166 | var buf []byte 167 | if modbusType == "RTU" { 168 | buf, err = ReadModbusRTUResponse(conn, regPkg) 169 | if err != nil { 170 | return 0, nil, err 171 | } 172 | } else if modbusType == "TCP" { 173 | buf, err = ReadModbusTCPResponse(conn, regPkg) 174 | if err != nil { 175 | return 0, nil, err 176 | } 177 | } else { 178 | return 0, nil, fmt.Errorf("unsupported modbus type") 179 | } 180 | n := len(buf) 181 | logrus.Info("regPkg:", regPkg, " 返回:", buf[:n]) 182 | return n, buf, nil 183 | } 184 | 185 | func sendRTUDataAndProcessResponse(conn net.Conn, data []byte, RTUCommand *modbus.RTUCommand, commandRaw *tpconfig.CommandRaw, regPkg string, tpSubDevice *api.SubDevice) (bool, error) { 186 | n, buf, err := sendDataAndReadResponse(conn, data, regPkg, "RTU") 187 | if err != nil { 188 | if err.Error() == "not supported function code" || err.Error() == "read failed" { 189 | return false, err 190 | } 191 | return true, err 192 | } 193 | 194 | respData, err := RTUCommand.ParseAndValidateResponse(buf[:n]) 195 | if err != nil { 196 | return false, err 197 | } 198 | 199 | dataMap, err := commandRaw.Serialize(respData) 200 | if err != nil { 201 | return false, err 202 | } 203 | 204 | payloadMap := map[string]interface{}{ 205 | "device_id": tpSubDevice.DeviceID, 206 | "values": dataMap, 207 | } 208 | var values []byte 209 | // 将payloadMap.values 转为json字符串 210 | values, err = json.Marshal(payloadMap["values"]) 211 | if err != nil { 212 | return false, err 213 | } 214 | logrus.Info("values:", string(values)) 215 | payloadMap["values"] = values 216 | payload, err := json.Marshal(payloadMap) 217 | if err != nil { 218 | return false, err 219 | } 220 | 221 | return false, MQTT.Publish(string(payload)) 222 | } 223 | 224 | // handleTCPCommand 处理TCPCommand 225 | func handleTCPCommand(TCPCommand *modbus.TCPCommand, commandRaw *tpconfig.CommandRaw, regPkg string, tpSubDevice *api.SubDevice, deviceID string) { 226 | data, err := TCPCommand.Serialize() 227 | if err != nil { 228 | logrus.Info("Error serializing TCPCommand:", err) 229 | return 230 | } 231 | 232 | m, exists := globaldata.DeviceConnectionMap.Load(deviceID) 233 | if !exists { 234 | logrus.Info("No connection found for regPkg:", regPkg, " deviceID:", deviceID) 235 | return 236 | } 237 | gatewayConn := m.(*net.Conn) 238 | conn := *gatewayConn 239 | defer CloseConnection(conn, deviceID) 240 | 241 | for { 242 | if isClose, err := sendTCPDataAndProcessResponse(conn, data, TCPCommand, commandRaw, regPkg, tpSubDevice); err != nil { 243 | if isClose { 244 | conn.Close() 245 | return 246 | } 247 | } 248 | time.Sleep(time.Duration(commandRaw.Interval) * time.Second) 249 | } 250 | } 251 | 252 | func sendTCPDataAndProcessResponse(conn net.Conn, data []byte, TCPCommand *modbus.TCPCommand, commandRaw *tpconfig.CommandRaw, regPkg string, tpSubDevice *api.SubDevice) (bool, error) { 253 | n, buf, err := sendDataAndReadResponse(conn, data, regPkg, "TCP") 254 | if err != nil { 255 | return true, err 256 | } 257 | respData, err := TCPCommand.ParseTCPResponse(buf[:n]) 258 | if err != nil { 259 | return false, err 260 | } 261 | dataMap, err := commandRaw.Serialize(respData) 262 | if err != nil { 263 | return false, err 264 | } 265 | 266 | payloadMap := map[string]interface{}{ 267 | "device_id": tpSubDevice.DeviceID, 268 | //"values": map[string]interface{}{tpSubDevice.SubDeviceAddr: dataMap}, 269 | "values": dataMap, 270 | } 271 | var values []byte 272 | // 将payloadMap.values 转为json字符串 273 | values, err = json.Marshal(payloadMap["values"]) 274 | if err != nil { 275 | return false, err 276 | } 277 | payloadMap["values"] = values 278 | payload, err := json.Marshal(payloadMap) 279 | if err != nil { 280 | return false, err 281 | } 282 | 283 | return false, MQTT.Publish(string(payload)) 284 | } 285 | 286 | // 开启线程处理RTUCommand 287 | func OneHandleRTUCommand(RTUCommand *modbus.RTUCommand, commandRaw *tpconfig.CommandRaw, regPkg string, tpSubDevice *api.SubDevice, deviceID string) { 288 | data, err := RTUCommand.Serialize() 289 | if err != nil { 290 | logrus.Info(err.Error()) 291 | return 292 | } 293 | 294 | m, exists := globaldata.DeviceConnectionMap.Load(deviceID) 295 | if !exists { 296 | logrus.Info("No connection found for regPkg:", regPkg, " deviceID:", deviceID) 297 | return 298 | } 299 | gatewayConn := m.(*net.Conn) 300 | conn := *gatewayConn 301 | defer CloseConnection(conn, deviceID) 302 | 303 | for { 304 | isClose, err := sendRTUDataAndProcessResponse(conn, data, RTUCommand, commandRaw, regPkg, tpSubDevice) 305 | if err != nil { 306 | logrus.Info("Error processing data:", err.Error()) 307 | if isClose { 308 | conn.Close() 309 | return 310 | } 311 | 312 | } 313 | // 间隔时间不能小于1秒 314 | if commandRaw.Interval < 1 { 315 | commandRaw.Interval = 1 316 | } 317 | time.Sleep(time.Duration(commandRaw.Interval) * time.Second) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /services/read_rtu_data.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "encoding/hex" 7 | "fmt" 8 | "net" 9 | 10 | "github.com/sirupsen/logrus" 11 | ) 12 | 13 | /* 14 | 逻辑: 15 | 一次请求必然有一次响应。 16 | 在发送请求前,也许缓冲区已经有了多个心跳包,也许没有心跳包。 17 | 程序需要跳过心跳包,来读取响应数据。 18 | 在一次读取的时候也许响应会跟在心跳包后。 19 | 只要有响应,就返回响应。 20 | ReadModbusRTUResponse方法返回失败会导致连接关闭,所以如果不是连接断开就不能返回错误。 21 | 不做CRC校验 22 | */ 23 | 24 | func isTimeout(err error) bool { 25 | if netErr, ok := err.(net.Error); ok { 26 | return netErr.Timeout() 27 | } 28 | return false 29 | } 30 | 31 | func ReadModbusRTUResponse(conn net.Conn, regPkg string) ([]byte, error) { 32 | var buffer bytes.Buffer 33 | readBuffer := make([]byte, 256) 34 | 35 | // 最多读取两次 36 | for i := 0; i < 2; i++ { 37 | n, err := conn.Read(readBuffer) 38 | logrus.Info("---------读取数据详情(第", i+1, "次)---------") 39 | logrus.Info("读取字节数: ", n) 40 | if n > 0 { 41 | logrus.Info("数据内容(hex): ", hex.EncodeToString(readBuffer[:n])) 42 | logrus.Info("数据内容(bytes): ", readBuffer[:n]) 43 | } 44 | if err != nil { 45 | logrus.Info("读取错误: ", err) 46 | logrus.Info("-----------------------------") 47 | if !isTimeout(err) { 48 | return nil, fmt.Errorf("连接异常: %v", err) 49 | } 50 | break 51 | } 52 | logrus.Info("-----------------------------") 53 | 54 | buffer.Write(readBuffer[:n]) 55 | 56 | // 尝试解析modbus响应 57 | if modbusData := findModbusResponse(buffer.Bytes()); modbusData != nil { 58 | return modbusData, nil 59 | } 60 | 61 | // 第一次没找到响应且没超时,继续读取 62 | if i == 0 && err == nil { 63 | continue 64 | } 65 | } 66 | 67 | return nil, nil 68 | } 69 | 70 | func findModbusResponse(data []byte) []byte { 71 | if len(data) < 5 { 72 | return nil 73 | } 74 | 75 | for i := 0; i < len(data)-4; i++ { 76 | if data[i] >= 0x30 && data[i] <= 0x39 { 77 | continue 78 | } 79 | 80 | if !isValidFunctionCode(data[i+1]) { 81 | continue 82 | } 83 | 84 | respLen, err := calculateResponseLength(data[i:]) 85 | if err != nil { 86 | continue 87 | } 88 | 89 | if i+respLen <= len(data) { 90 | return data[i : i+respLen] 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | func isValidFunctionCode(code byte) bool { 97 | validCodes := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x0F, 0x10} 98 | for _, valid := range validCodes { 99 | if code == valid { 100 | return true 101 | } 102 | } 103 | return false 104 | } 105 | 106 | func calculateResponseLength(header []byte) (int, error) { 107 | if header[1]&0x80 != 0 { 108 | return 5, nil // 异常响应固定5字节 109 | } 110 | 111 | switch header[1] { 112 | case 0x01, 0x02: 113 | return int(header[2]) + 5, nil 114 | case 0x03, 0x04: 115 | return int(header[2]) + 5, nil 116 | case 0x05, 0x06, 0x0F, 0x10: 117 | return 8, nil 118 | default: 119 | return 0, fmt.Errorf("不支持的功能码: %02X", header[1]) 120 | } 121 | } 122 | func ReadHeader(reader *bufio.Reader) ([]byte, error) { 123 | header, err := reader.Peek(3) 124 | if err != nil { 125 | return nil, err 126 | } 127 | _, err = reader.Discard(3) 128 | return header, err 129 | } 130 | -------------------------------------------------------------------------------- /services/read_tcp_data.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/hex" 7 | "fmt" 8 | "io" 9 | "net" 10 | 11 | "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func ReadModbusTCPResponse(conn net.Conn, regPkg string) ([]byte, error) { 15 | var heartbeatResponse []byte 16 | 17 | // 第一个字节预读 18 | firstByte := make([]byte, 1) 19 | _, err := io.ReadFull(conn, firstByte) 20 | if err != nil { 21 | logrus.Warn("读取第一个字节失败:", err) 22 | return nil, fmt.Errorf("读取数据失败") 23 | } 24 | 25 | // 快速判断是否可能是心跳包(通过第一个字节特征) 26 | if regPkg != "" && firstByte[0] == regPkg[0] { 27 | // 可能是心跳包,继续读取剩余部分 28 | expectedBytes, _ := hex.DecodeString(regPkg) 29 | if len(expectedBytes) > 1 { 30 | remainingBytes := make([]byte, len(expectedBytes)-1) 31 | _, err = io.ReadFull(conn, remainingBytes) 32 | if err != nil { 33 | logrus.Warn("读取心跳包剩余数据失败:", err) 34 | return nil, fmt.Errorf("读取数据失败") 35 | } 36 | 37 | heartbeatResponse = append(firstByte, remainingBytes...) 38 | if bytes.Equal(heartbeatResponse, expectedBytes) { 39 | logrus.Debug("成功读取到心跳包响应") 40 | } 41 | } 42 | 43 | // 继续读取 Modbus 响应的第一个字节 44 | _, err = io.ReadFull(conn, firstByte) 45 | if err != nil { 46 | if heartbeatResponse != nil { 47 | // 如果已经读到心跳包,就返回心跳包 48 | return heartbeatResponse, nil 49 | } 50 | logrus.Warn("读取 Modbus 响应第一个字节失败:", err) 51 | return nil, fmt.Errorf("读取数据失败") 52 | } 53 | } 54 | 55 | // 读取剩余的 MBAP 头部(7字节) 56 | header := make([]byte, 7) 57 | _, err = io.ReadFull(conn, header) 58 | if err != nil { 59 | if heartbeatResponse != nil { 60 | // 如果已经读到心跳包,就返回心跳包 61 | return heartbeatResponse, nil 62 | } 63 | logrus.Warn("读取 Modbus TCP 报文头失败:", err) 64 | return nil, fmt.Errorf("读取报文头失败") 65 | } 66 | 67 | // 组合完整的头部(8字节) 68 | fullHeader := append(firstByte, header...) 69 | 70 | // 解析 MBAP 头 71 | length := binary.BigEndian.Uint16(fullHeader[4:6]) 72 | functionCode := fullHeader[7] 73 | 74 | // 计算需要读取的数据长度 75 | dataLength := int(length) - 2 // 减去单元ID和功能码长度 76 | if dataLength < 0 || dataLength > 256 { 77 | if heartbeatResponse != nil { 78 | // 如果已经读到心跳包,就返回心跳包 79 | return heartbeatResponse, nil 80 | } 81 | logrus.Warn("无效的数据长度") 82 | return nil, fmt.Errorf("无效的数据长度") 83 | } 84 | 85 | // 读取数据部分 86 | data := make([]byte, dataLength) 87 | _, err = io.ReadFull(conn, data) 88 | if err != nil { 89 | if heartbeatResponse != nil { 90 | // 如果已经读到心跳包,就返回心跳包 91 | return heartbeatResponse, nil 92 | } 93 | logrus.Warn("读取响应数据失败:", err) 94 | return nil, fmt.Errorf("读取响应数据失败") 95 | } 96 | 97 | // 组合完整响应 98 | modbusResponse := append(fullHeader, data...) 99 | logrus.Debugf("收到 Modbus 响应: 功能码=0x%02X, 数据长度=%d", functionCode, len(modbusResponse)) 100 | 101 | // 如果有心跳包响应,将其与 Modbus 响应组合 102 | if heartbeatResponse != nil { 103 | finalResponse := append(heartbeatResponse, modbusResponse...) 104 | return finalResponse, nil 105 | } 106 | 107 | return modbusResponse, nil 108 | } 109 | -------------------------------------------------------------------------------- /services/services.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "net" 5 | "strings" 6 | "sync" 7 | 8 | httpclient "github.com/ThingsPanel/modbus-protocol-plugin/http_client" 9 | "github.com/sirupsen/logrus" 10 | 11 | globaldata "github.com/ThingsPanel/modbus-protocol-plugin/global_data" 12 | MQTT "github.com/ThingsPanel/modbus-protocol-plugin/mqtt" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | // 定义全局的conn管道 17 | var connChan = make(chan net.Conn) 18 | 19 | // 简单的IP限制 20 | var ( 21 | blockedIPs = make(map[string]bool) 22 | ipMutex = &sync.Mutex{} 23 | ) 24 | 25 | func Start() { 26 | // 启动处理连接的goroutine 27 | go handleChanConnections() 28 | // 启动服务 29 | go startServer() 30 | } 31 | 32 | // startServer启动服务 33 | func startServer() { 34 | serverAddr := viper.GetString("server.address") 35 | listen, err := net.Listen("tcp", serverAddr) 36 | if err != nil { 37 | logrus.Info("Listen() failed, err: ", err) 38 | return 39 | } 40 | logrus.Info("modbus服务启动成功:", serverAddr) 41 | for { 42 | conn, err := listen.Accept() // 监听客户端的连接请求 43 | if err != nil { 44 | logrus.Info("Accept() failed, err: ", err) 45 | continue 46 | } 47 | 48 | // 检查IP是否被限制 49 | clientIP := strings.Split(conn.RemoteAddr().String(), ":")[0] 50 | ipMutex.Lock() 51 | blocked := blockedIPs[clientIP] 52 | ipMutex.Unlock() 53 | 54 | if blocked { 55 | conn.Close() 56 | continue 57 | } 58 | 59 | // 将接受的conn写入管道 60 | connChan <- conn 61 | } 62 | } 63 | 64 | // handleConnections处理来自管道的连接 65 | func handleChanConnections() { 66 | for { 67 | conn := <-connChan 68 | go verifyConnection(conn) // 处理每个连接的具体逻辑 69 | } 70 | } 71 | 72 | func CloseConnection(conn net.Conn, regPkg string) { 73 | err := conn.Close() 74 | if err != nil { 75 | logrus.Info("Close() failed, err: ", err) 76 | } 77 | // 删除全局变量 78 | if m, exists := globaldata.DeviceConnectionMap.Load(regPkg); !exists { 79 | return 80 | } else if conn != *m.(*net.Conn) { 81 | return 82 | } 83 | logrus.Info("删除全局变量完成:", regPkg) 84 | // 做其他事情,比如发送离线消息 85 | m := *MQTT.MqttClient 86 | err = m.SendStatus(regPkg, "0") 87 | if err != nil { 88 | logrus.Info("SendStatus() failed, err: ", err) 89 | } 90 | globaldata.GateWayConfigMap.Delete(regPkg) 91 | globaldata.DeviceConnectionMap.Delete(regPkg) 92 | delete(globaldata.DeviceRWLock, regPkg) 93 | // 设备离线 94 | logrus.Info("设备离线:", regPkg) 95 | } 96 | 97 | // 验证连接并继续处理数据 98 | func verifyConnection(conn net.Conn) { 99 | // 读取客户端发送的数据 100 | var buf [1024]byte 101 | n, err := conn.Read(buf[:]) 102 | if err != nil { 103 | // 如果是连接重置错误,将IP加入黑名单 104 | if strings.Contains(err.Error(), "connection reset by peer") { 105 | clientIP := strings.Split(conn.RemoteAddr().String(), ":")[0] 106 | ipMutex.Lock() 107 | blockedIPs[clientIP] = true 108 | ipMutex.Unlock() 109 | logrus.Info("IP已被限制: ", clientIP) 110 | } else { 111 | logrus.Info("Read() failed, err: ", err) 112 | } 113 | conn.Close() 114 | return 115 | } 116 | regPkg := string(buf[:n]) 117 | logrus.Info("收到客户端发来的注册包:", regPkg) 118 | // 首次接收到的是设备regPkg,需要根据regPkg获取设备配置 119 | // 凭借voucher 120 | voucher := `{"reg_pkg":"` + regPkg + `"}` 121 | // 读取设备配置 122 | tpGatewayConfig, err := httpclient.GetDeviceConfig(voucher, "") 123 | if err != nil { 124 | // 获取设备配置失败,请检查连接包是否正确 125 | logrus.Error(err) 126 | conn.Close() 127 | return 128 | } 129 | logrus.Info("获取设备配置成功:", tpGatewayConfig) 130 | // 将平台网关的配置存入全局变量 131 | globaldata.GateWayConfigMap.Store(tpGatewayConfig.Data.ID, &tpGatewayConfig.Data) 132 | // 将设备连接存入全局变量 133 | globaldata.DeviceConnectionMap.Store(tpGatewayConfig.Data.ID, &conn) 134 | m := *MQTT.MqttClient 135 | err = m.SendStatus(tpGatewayConfig.Data.ID, "1") 136 | if err != nil { 137 | logrus.Info("SendStatus() failed, err: ", err) 138 | } 139 | // 设备上线 140 | logrus.Info("设备上线(", tpGatewayConfig.Data.ID, "):", regPkg) 141 | HandleConn(regPkg, tpGatewayConfig.Data.ID) // 处理连接 142 | // defer conn.Close() 143 | } 144 | -------------------------------------------------------------------------------- /tp_config/command_raw.go: -------------------------------------------------------------------------------- 1 | package tpconfig 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "math" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/Knetic/govaluate" 11 | globaldata "github.com/ThingsPanel/modbus-protocol-plugin/global_data" 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type CommandRaw struct { 16 | FunctionCode byte // 功能码 17 | StartingAddress uint16 // 起始地址 18 | Quantity uint16 // 寄存器数量或数据数量 19 | Endianess string // 大端或小端 BIG or LITTLE 20 | 21 | Interval int // 采集时间间隔 22 | DataType string // 数据类型 int16, uint16, int32, uint32, float32, float64 23 | DataIdetifierListStr string // 数据标识符 例如:A1, A2, A3... 24 | EquationListStr string // 公式 例如:A1*0.1, A2*0.2, A3*0.3... 25 | DecimalPlacesListStr string // 小数位数 例如:1, 2, 3... 26 | } 27 | 28 | // NewCommandRaw 创建CommandRaw 29 | func NewCommandRaw(commandRawMap map[string]interface{}) (*CommandRaw, error) { 30 | // 类型断言之前,先确保该key存在并且值的类型正确 31 | functionCode, ok := commandRawMap["FunctionCode"].(float64) 32 | if !ok { 33 | return nil, fmt.Errorf("functionCode is either missing or of incorrect type") 34 | } 35 | 36 | startingAddress, ok := commandRawMap["StartingAddress"].(float64) 37 | if !ok { 38 | return nil, fmt.Errorf("startingAddress is either missing or of incorrect type") 39 | } 40 | 41 | quantity, ok := commandRawMap["Quantity"].(float64) 42 | if !ok { 43 | return nil, fmt.Errorf("quantity is either missing or of incorrect type") 44 | } 45 | 46 | endianess, ok := commandRawMap["Endianess"].(string) 47 | if !ok { 48 | return nil, fmt.Errorf("endianess is either missing or of incorrect type") 49 | } 50 | 51 | interval, ok := commandRawMap["Interval"].(float64) 52 | if !ok { 53 | return nil, fmt.Errorf("interval is either missing or of incorrect type") 54 | } 55 | 56 | dataType, ok := commandRawMap["DataType"].(string) 57 | if !ok { 58 | return nil, fmt.Errorf("dataType is either missing or of incorrect type") 59 | } 60 | 61 | dataIdetifierListStr, ok := commandRawMap["DataIdentifierListStr"].(string) 62 | if !ok { 63 | return nil, fmt.Errorf("dataIdetifierListStr is either missing or of incorrect type") 64 | } 65 | 66 | equationListStr, ok := commandRawMap["EquationListStr"].(string) 67 | if !ok { 68 | equationListStr = "" 69 | logrus.Warn("equationListStr is either missing or of incorrect type, set to empty string") 70 | } 71 | 72 | decimalPlacesListStr, ok := commandRawMap["DecimalPlacesListStr"].(string) 73 | if !ok { 74 | decimalPlacesListStr = "" 75 | logrus.Warn("decimalPlacesListStr is either missing or of incorrect type, set to empty string") 76 | } 77 | // ... repeat the same for other fields ... 78 | 79 | return &CommandRaw{ 80 | FunctionCode: byte(functionCode), 81 | StartingAddress: uint16(startingAddress), 82 | Quantity: uint16(quantity), 83 | Endianess: endianess, 84 | Interval: int(interval), 85 | DataType: dataType, 86 | DataIdetifierListStr: dataIdetifierListStr, 87 | EquationListStr: equationListStr, 88 | DecimalPlacesListStr: decimalPlacesListStr, 89 | }, nil 90 | } 91 | 92 | // 根据CommandRaw计算写报文的功能码、起始地址、数据 93 | func (c *CommandRaw) GetWriteCommand(key string, value interface{}, index int) (byte, uint16, []byte, error) { 94 | var functionCode byte 95 | var startingAddress uint16 96 | var data []byte 97 | // 找到 98 | switch c.FunctionCode { 99 | case 0x01: 100 | functionCode = 0x05 101 | case 0x02: 102 | functionCode = 0x05 103 | case 0x03: 104 | functionCode = 0x06 105 | case 0x04: 106 | functionCode = 0x06 107 | } 108 | // 根据c.StartingAddress、index和c.DataType 109 | // 计算出写报文的起始地址和数据 110 | switch c.DataType { 111 | case "int16": 112 | startingAddress = c.StartingAddress + uint16(index)*2 113 | data = make([]byte, 2) 114 | if c.Endianess == "LITTLE" { 115 | binary.LittleEndian.PutUint16(data, uint16(value.(float64))) 116 | } else { 117 | binary.BigEndian.PutUint16(data, uint16(value.(float64))) 118 | } 119 | case "uint16": 120 | startingAddress = c.StartingAddress + uint16(index)*2 121 | data = make([]byte, 2) 122 | if c.Endianess == "LITTLE" { 123 | binary.LittleEndian.PutUint16(data, uint16(value.(float64))) 124 | } else { 125 | binary.BigEndian.PutUint16(data, uint16(value.(float64))) 126 | } 127 | case "int32": 128 | startingAddress = c.StartingAddress + uint16(index)*4 129 | data = make([]byte, 4) 130 | if c.Endianess == "LITTLE" { 131 | binary.LittleEndian.PutUint32(data, uint32(value.(int32))) 132 | } else { 133 | binary.BigEndian.PutUint32(data, uint32(value.(int32))) 134 | } 135 | case "uint32": 136 | startingAddress = c.StartingAddress + uint16(index)*4 137 | data = make([]byte, 4) 138 | if c.Endianess == "LITTLE" { 139 | binary.LittleEndian.PutUint32(data, uint32(value.(int32))) 140 | } else { 141 | binary.BigEndian.PutUint32(data, uint32(value.(int32))) 142 | } 143 | case "float32": 144 | startingAddress = c.StartingAddress + uint16(index)*4 145 | data = make([]byte, 4) 146 | if c.Endianess == "LITTLE" { 147 | bits := math.Float32bits(float32(value.(float64))) 148 | binary.LittleEndian.PutUint32(data, bits) 149 | } else { 150 | bits := math.Float32bits(float32(value.(float64))) 151 | binary.BigEndian.PutUint32(data, bits) 152 | } 153 | case "float64": 154 | startingAddress = c.StartingAddress + uint16(index)*8 155 | data = make([]byte, 8) 156 | if c.Endianess == "LITTLE" { 157 | bits := math.Float64bits(value.(float64)) 158 | binary.LittleEndian.PutUint64(data, bits) 159 | } else { 160 | bits := math.Float64bits(value.(float64)) 161 | binary.BigEndian.PutUint64(data, bits) 162 | } 163 | case "coil": 164 | startingAddress = c.StartingAddress + uint16(index) 165 | 166 | val, ok := value.(float64) 167 | if !ok { 168 | // 返回类型错误或其他错误处理逻辑 169 | return functionCode, startingAddress, data, fmt.Errorf("expected float64 value for coil, got %T", value) 170 | } 171 | 172 | if val == 1.0 { 173 | data = []byte{0xFF, 0x00} 174 | } else if val == 0.0 { 175 | data = []byte{0x00, 0x00} 176 | } else { 177 | // 返回无效值错误或其他错误处理逻辑 178 | return functionCode, startingAddress, data, fmt.Errorf("invalid coil value: %f", val) 179 | } 180 | } 181 | return functionCode, startingAddress, data, nil 182 | 183 | } 184 | 185 | // 将modbus返回的数据序列化为json报文 186 | func (c *CommandRaw) Serialize(resp []byte) (map[string]interface{}, error) { 187 | if len(resp) < 4 { 188 | return nil, fmt.Errorf("invalid response length: %d", len(resp)) 189 | } 190 | // 检查Modbus异常响应 191 | if resp[1] >= byte(0x80) { 192 | // 错误码映射 193 | err := fmt.Errorf("function Code(0x%02x) exception Code(0x%02x):%s", resp[1], resp[2], globaldata.GetModbusErrorDesc(resp[2])) 194 | logrus.Error(err) 195 | return nil, err 196 | } 197 | 198 | data := resp[3:] // 过滤Modbus地址、功能码和字节计数 199 | values := make(map[string]interface{}) 200 | 201 | // Choose the right byte order based on the Endianess attribute 202 | var byteOrder binary.ByteOrder 203 | if c.Endianess == "LITTLE" { 204 | byteOrder = binary.LittleEndian 205 | } else if c.Endianess == "BIG" { 206 | byteOrder = binary.BigEndian 207 | } else { 208 | return nil, fmt.Errorf("unknown endianess specified") 209 | } 210 | 211 | dataIds := strings.Split(c.DataIdetifierListStr, ",") 212 | byteIndex := 0 213 | for _, id := range dataIds { 214 | var val float64 215 | id = strings.TrimSpace(id) 216 | switch c.DataType { 217 | case "int16": 218 | val = float64(int16(byteOrder.Uint16(data[byteIndex : byteIndex+2]))) 219 | byteIndex += 2 220 | case "uint16": 221 | val = float64(byteOrder.Uint16(data[byteIndex : byteIndex+2])) 222 | byteIndex += 2 223 | case "int32": 224 | val = float64(int32(byteOrder.Uint32(data[byteIndex : byteIndex+4]))) 225 | byteIndex += 4 226 | case "uint32": 227 | val = float64(byteOrder.Uint32(data[byteIndex : byteIndex+4])) 228 | byteIndex += 4 229 | case "float32": 230 | var floatVal float32 231 | bits := byteOrder.Uint32(data[byteIndex : byteIndex+4]) 232 | floatVal = math.Float32frombits(bits) 233 | val = float64(floatVal) 234 | byteIndex += 4 235 | case "float64": 236 | val = math.Float64frombits(byteOrder.Uint64(data[byteIndex : byteIndex+8])) 237 | byteIndex += 8 238 | case "coil": 239 | // Assuming each coil is a single bit and we may need to read multiple coils 240 | // stored in consecutive bits of bytes. 241 | coilVal := (data[byteIndex/8] >> (byteIndex % 8)) & 0x01 // Extract the bit at the correct position 242 | val = float64(coilVal) 243 | byteIndex++ // Move to the next bit 244 | } 245 | 246 | values[id] = val 247 | } 248 | 249 | // 以下是公式处理和小数处理 250 | 251 | decimalPlacesList := strings.Split(c.DecimalPlacesListStr, ",") 252 | equations := strings.Split(c.EquationListStr, ",") 253 | 254 | singleDecimalPlace := false 255 | singleEquation := false 256 | 257 | // 如果只有一个小数位,将其应用于所有数据 258 | if len(decimalPlacesList) == 1 { 259 | singleDecimalPlace = true 260 | } 261 | 262 | // 如果只有一个公式,将其应用于所有数据 263 | if len(equations) == 1 { 264 | singleEquation = true 265 | } 266 | 267 | for i, id := range dataIds { 268 | id = strings.TrimSpace(id) 269 | 270 | // 处理公式 271 | if c.EquationListStr != "" && (i < len(equations) || singleEquation) { 272 | eqIndex := i 273 | if singleEquation { 274 | eqIndex = 0 275 | } 276 | 277 | expression, err := govaluate.NewEvaluableExpression(equations[eqIndex]) 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | result, err := expression.Evaluate(values) 283 | if err != nil { 284 | return nil, err 285 | } 286 | 287 | resFloat, ok := result.(float64) 288 | if !ok { 289 | return nil, fmt.Errorf("result of equation is not float64") 290 | } 291 | values[id] = resFloat 292 | } 293 | 294 | // 处理小数位数 295 | if c.DecimalPlacesListStr != "" && (i < len(decimalPlacesList) || singleDecimalPlace) { 296 | placeIndex := i 297 | if singleDecimalPlace { 298 | placeIndex = 0 299 | } 300 | 301 | places, err := strconv.Atoi(strings.TrimSpace(decimalPlacesList[placeIndex])) 302 | if err != nil { 303 | logrus.Info("invalid decimal place value for ", id, ": ", err) 304 | continue 305 | } 306 | 307 | multiplier := math.Pow(10, float64(places)) 308 | if val, ok := values[id].(float64); ok { 309 | values[id] = math.Round(val*multiplier) / multiplier 310 | } else { 311 | logrus.Info("value of ", id, " is not float64") 312 | continue 313 | } 314 | } 315 | } 316 | 317 | return values, nil 318 | } 319 | -------------------------------------------------------------------------------- /tp_config/sub_device_form_config.go: -------------------------------------------------------------------------------- 1 | package tpconfig 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/sirupsen/logrus" 8 | ) 9 | 10 | type SubDeviceFormConfig struct { 11 | SlaveID uint8 12 | CommandRawList []*CommandRaw 13 | } 14 | 15 | func NewSubDeviceFormConfig(formConfigMap map[string]interface{}, subDeviceAddr string) (*SubDeviceFormConfig, error) { 16 | // SlaveID 17 | var slaveIDFloat float64 18 | slaveIDInterface, exists := formConfigMap["SlaveID"] 19 | if !exists || slaveIDInterface == "" { 20 | // 如果 SlaveID 不存在,使用 subDeviceAddr 21 | s, err := strconv.ParseFloat(subDeviceAddr, 64) 22 | if err != nil { 23 | logrus.Error("子设备地址不是有效的数字格式:", err) 24 | return nil, fmt.Errorf("子设备地址必须是有效的数字格式") 25 | } 26 | slaveIDFloat = s 27 | } else { 28 | // 尝试从 formConfigMap 中获取 SlaveID 29 | if floatVal, ok := slaveIDInterface.(float64); ok { 30 | slaveIDFloat = floatVal 31 | } else if strVal, ok := slaveIDInterface.(string); ok { 32 | // 如果是字符串,尝试转换为数字 33 | s, err := strconv.ParseFloat(strVal, 64) 34 | if err != nil { 35 | logrus.Error("从配置中获取的从站ID不是有效的数字格式:", err) 36 | return nil, fmt.Errorf("从站ID必须是有效的数字格式") 37 | } 38 | slaveIDFloat = s 39 | } else { 40 | logrus.Error("从站ID格式无效") 41 | return nil, fmt.Errorf("从站ID格式无效") 42 | } 43 | } 44 | 45 | // CommandRawList 46 | commandRawListInterface, ok := formConfigMap["CommandRawList"].([]interface{}) 47 | if !ok { 48 | logrus.Error("命令列表格式无效") 49 | return nil, fmt.Errorf("命令列表格式无效") 50 | } 51 | 52 | var commandRawList []*CommandRaw 53 | for _, commandRawMapInterface := range commandRawListInterface { 54 | commandRawMap, ok := commandRawMapInterface.(map[string]interface{}) 55 | if !ok { 56 | logrus.Error("命令项格式无效") 57 | return nil, fmt.Errorf("命令项格式无效") 58 | } 59 | 60 | commandRaw, err := NewCommandRaw(commandRawMap) 61 | if err != nil { 62 | logrus.Error("创建命令对象失败:", err) 63 | continue 64 | } 65 | commandRawList = append(commandRawList, commandRaw) 66 | } 67 | 68 | return &SubDeviceFormConfig{ 69 | SlaveID: uint8(slaveIDFloat), 70 | CommandRawList: commandRawList, 71 | }, nil 72 | } 73 | -------------------------------------------------------------------------------- /v3.0设计.md: -------------------------------------------------------------------------------- 1 | # 3.0设计 2 | 3 | 1. 内存数据结构 4 | a) 请求映射表: 5 | - 使用 sync.Map 存储未完成的请求 6 | - 键:请求ID (uint64) 7 | - 值:请求详情结构体(包含子设备ID、功能码、数据地址等) 8 | 9 | b) 设备配置表: 10 | - 使用 map[uint16]*DeviceConfig 存储子设备配置 11 | - 键:子设备ID 12 | - 值:设备配置结构体(包含Modbus地址、超时设置等) 13 | 14 | c) 请求队列: 15 | - 使用 channel 实现线程安全的请求队列 16 | - 类型:chan *ModbusRequest 17 | 18 | d) 响应队列: 19 | - 使用 channel 实现线程安全的响应队列 20 | - 类型:chan *ModbusResponse 21 | 22 | 2. 优化后的详细过程: 23 | 24 | 1. 请求队列 25 | - 使用 buffered channel 作为请求队列 26 | - 定义 ModbusRequest 结构体,包含所有必要字段 27 | 28 | 2. 请求处理器 29 | - 使用 for-select 循环处理请求队列 30 | - 使用 atomic 包生成唯一的请求ID 31 | - 使用 sync.Map 存储请求信息 32 | 33 | 3. DTU 网关通信 34 | - 使用 sync.Mutex 保护发送过程 35 | - 实现指数退避的重连机制 36 | 37 | 4. 响应接收器 38 | - 使用 bufio.Reader 提高读取效率 39 | - 实现帧同步机制,处理不完整或错误的帧 40 | 41 | 5. 响应队列 42 | - 使用 buffered channel 作为响应队列 43 | 44 | 6. 响应处理器 45 | - 使用 for-select 循环处理响应队列 46 | - 使用 sync.Map 的 LoadAndDelete 方法原子性地查找和删除请求 47 | 48 | 7. 超时处理 49 | - 使用 time.Ticker 定期检查超时 50 | - 将超时检查集成到响应处理循环中,减少 goroutine 数量 51 | 52 | 8. 请求发起和结果获取 53 | - 使用 context.Context 管理超时 54 | - 返回一个 channel 用于接收结果,而不是阻塞等待 55 | 56 | 9. 错误处理和恢复机制 57 | - 使用错误计数器,在连续失败次数超过阈值时触发重连 58 | - 实现环形缓冲区记录最近的错误日志 59 | 60 | 10. 并发控制 61 | - 使用 semaphore 限制并发请求数量 62 | - 实现简单的令牌桶算法进行流量控制 63 | 64 | 11. 状态监控 65 | - 使用原子操作维护关键统计信息(如请求计数、成功率等) 66 | - 实现一个简单的内存统计功能,监控关键数据结构的大小 67 | 68 | 3. 内存管理优化: 69 | - 实现对象池,重用 ModbusRequest 和 ModbusResponse 对象 70 | - 定期运行 runtime.GC() 来主动触发垃圾回收 71 | 72 | 4. 并发安全优化: 73 | - 尽可能使用 channel 和 atomic 操作代替互斥锁,提高并发性 74 | - 对于读多写少的数据,使用 sync.RWMutex 代替 sync.Mutex 75 | 76 | 5. 性能优化: 77 | - 使用 ring buffer 预分配内存,减少动态内存分配 78 | - 实现批处理机制,在可能的情况下合并多个请求 79 | 80 | 6. 错误恢复增强: 81 | - 实现熔断器模式,在检测到持续错误时暂时停止向某个子设备发送请求 82 | - 使用指数退避算法进行重试,避免立即重试对系统造成额外负担 83 | 84 | 7. 可观测性增强: 85 | - 实现一个简单的内存中的度量收集器,记录关键性能指标 86 | - 提供一个 HTTP 端点,用于查询当前系统状态和性能指标 87 | 88 | 8. 资源管理: 89 | - 实现 goroutine 池,重用 goroutine 来处理请求和响应,避免频繁创建和销毁 90 | - 使用 sync.Pool 来重用大型缓冲区,减少内存分配和GC压力 91 | 92 | 示例代码结构(部分关键组件): 93 | 94 | ```go 95 | type ModbusGateway struct { 96 | requestQueue chan *ModbusRequest 97 | responseQueue chan *ModbusResponse 98 | pendingRequests sync.Map 99 | deviceConfigs map[uint16]*DeviceConfig 100 | conn net.Conn 101 | sendMutex sync.Mutex 102 | // 其他字段... 103 | } 104 | 105 | func NewModbusGateway() *ModbusGateway { 106 | // 初始化网关 107 | } 108 | 109 | func (gw *ModbusGateway) Start() { 110 | go gw.processRequests() 111 | go gw.handleResponses() 112 | go gw.monitorTimeouts() 113 | // 启动其他必要的goroutine... 114 | } 115 | 116 | func (gw *ModbusGateway) processRequests() { 117 | for req := range gw.requestQueue { 118 | // 处理请求... 119 | } 120 | } 121 | 122 | func (gw *ModbusGateway) handleResponses() { 123 | for resp := range gw.responseQueue { 124 | // 处理响应... 125 | } 126 | } 127 | 128 | func (gw *ModbusGateway) monitorTimeouts() { 129 | ticker := time.NewTicker(100 * time.Millisecond) 130 | for range ticker.C { 131 | // 检查并处理超时请求... 132 | } 133 | } 134 | 135 | func (gw *ModbusGateway) SendRequest(ctx context.Context, req *ModbusRequest) <-chan *ModbusResponse { 136 | // 发送请求并返回接收结果的channel... 137 | } 138 | ``` 139 | 140 | 这个优化后的方案充分利用了Go语言的并发特性和内存数据结构,无需数据库和持久化,同时保持了高效性和可靠性。它能够处理大量并发请求,同时有效管理内存使用和错误恢复。这个设计为未来的扩展(如添加优先级队列或实现更复杂的流控机制)提供了良好的基础。 141 | -------------------------------------------------------------------------------- /v3.0设计2.md: -------------------------------------------------------------------------------- 1 | 2 | # 设计2 3 | 4 | 1. 核心组件 5 | 6 | a) 请求队列:用于存储待处理的请求 7 | b) 映射表:用于匹配请求和响应 8 | c) 单一通信协程:处理请求发送和响应接收 9 | 10 | 2. 数据结构 11 | 12 | ```go 13 | type ModbusRequest struct { 14 | ID uint64 15 | SlaveID byte 16 | FunctionCode byte 17 | Address uint16 18 | Quantity uint16 19 | Data []byte 20 | ResponseChan chan *ModbusResponse 21 | } 22 | 23 | type ModbusResponse struct { 24 | ID uint64 25 | Data []byte 26 | Error error 27 | } 28 | 29 | type ModbusGateway struct { 30 | requestQueue chan *ModbusRequest 31 | conn net.Conn 32 | requests sync.Map 33 | nextID uint64 34 | mu sync.Mutex 35 | } 36 | ``` 37 | 38 | 3. 主要流程 39 | 40 | ```go 41 | func (gw *ModbusGateway) Start() { 42 | go gw.communicationLoop() 43 | } 44 | 45 | func (gw *ModbusGateway) communicationLoop() { 46 | for { 47 | select { 48 | case req := <-gw.requestQueue: 49 | gw.sendRequest(req) 50 | default: 51 | gw.readResponse() 52 | } 53 | } 54 | } 55 | 56 | func (gw *ModbusGateway) sendRequest(req *ModbusRequest) { 57 | gw.mu.Lock() 58 | req.ID = gw.nextID 59 | gw.nextID++ 60 | gw.mu.Unlock() 61 | 62 | gw.requests.Store(req.ID, req) 63 | // 发送请求到DTU网关 64 | // ... 65 | } 66 | 67 | func (gw *ModbusGateway) readResponse() { 68 | // 从DTU网关读取响应 69 | // ... 70 | 71 | // 假设我们已经解析出响应,并获得了对应的请求ID 72 | if req, ok := gw.requests.LoadAndDelete(respID); ok { 73 | request := req.(*ModbusRequest) 74 | request.ResponseChan <- &ModbusResponse{ 75 | ID: respID, 76 | Data: respData, 77 | } 78 | } 79 | } 80 | 81 | func (gw *ModbusGateway) SendRequest(ctx context.Context, slaveID byte, functionCode byte, address uint16, quantity uint16, data []byte) (*ModbusResponse, error) { 82 | responseChan := make(chan *ModbusResponse, 1) 83 | req := &ModbusRequest{ 84 | SlaveID: slaveID, 85 | FunctionCode: functionCode, 86 | Address: address, 87 | Quantity: quantity, 88 | Data: data, 89 | ResponseChan: responseChan, 90 | } 91 | 92 | select { 93 | case gw.requestQueue <- req: 94 | case <-ctx.Done(): 95 | return nil, ctx.Err() 96 | } 97 | 98 | select { 99 | case resp := <-responseChan: 100 | return resp, resp.Error 101 | case <-ctx.Done(): 102 | gw.requests.Delete(req.ID) 103 | return nil, ctx.Err() 104 | } 105 | } 106 | ``` 107 | 108 | 4. 超时处理 109 | 110 | ```go 111 | func (gw *ModbusGateway) cleanupRoutine() { 112 | ticker := time.NewTicker(5 * time.Second) 113 | for range ticker.C { 114 | now := time.Now() 115 | gw.requests.Range(func(key, value interface{}) bool { 116 | req := value.(*ModbusRequest) 117 | if now.Sub(req.Timestamp) > 30*time.Second { 118 | gw.requests.Delete(key) 119 | req.ResponseChan <- &ModbusResponse{Error: errors.New("request timeout")} 120 | } 121 | return true 122 | }) 123 | } 124 | } 125 | ``` 126 | 127 | 5. 关键特点 128 | - 使用单一的通信循环处理发送和接收,简化了并发模型 129 | - 保留了映射表(使用 sync.Map),但简化了其使用 130 | - 每个请求有唯一ID,便于匹配响应 131 | - 使用 channel 进行请求队列和响应传递,保证了并发安全 132 | - 简单的超时清理机制 133 | 134 | 6. 优点 135 | 136 | - 结构清晰:单一的通信循环使逻辑更加集中和清晰 137 | - 并发安全:使用 sync.Map 和 channel 确保并发操作的安全性 138 | - 灵活性:保留映射表允许处理乱序响应 139 | - 简洁:相比完整的队列系统,这个方案更加精简 140 | 141 | 7. 注意事项 142 | 143 | - 错误处理:需要在发送和接收过程中加入适当的错误处理逻辑 144 | - 连接管理:应添加重连机制以处理网络断开的情况 145 | - 性能优化:在高并发场景下,可能需要调整缓冲区大小和超时清理频率 146 | 147 | 这个方案保持了映射表的优势,同时通过单一的通信循环简化了整体结构。它提供了一个好的平衡点,既保证了系统的可靠性和灵活性,又保持了代码的简洁性。根据您的具体需求,可以在此基础上进行进一步的优化和扩展。 148 | --------------------------------------------------------------------------------