├── .github └── workflows │ └── go-full-test.yml ├── .gitignore ├── LICENSE ├── README.md ├── README_zh_CN.md ├── client ├── core.go ├── errors.go ├── handle.go └── interface.go ├── go.mod ├── go.sum ├── pkg └── waitgroup │ └── waitgroup.go ├── server ├── core.go ├── handle.go └── interface.go └── tests ├── client_handle.go ├── iec104_test.go └── server_handle.go /.github/workflows/go-full-test.yml: -------------------------------------------------------------------------------- 1 | name: Go Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | paths-ignore: 8 | - 'docs/**' 9 | - '**/*.md' 10 | pull_request: 11 | branches: 12 | - master 13 | paths-ignore: 14 | - 'docs/**' 15 | - '**/*.md' 16 | 17 | jobs: 18 | static-analysis: 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - name: Check out repository 23 | uses: actions/checkout@v3 24 | 25 | - name: Set up Go 26 | uses: actions/setup-go@v4 27 | with: 28 | go-version: '1.20' 29 | 30 | - name: Cache Go modules 31 | uses: actions/cache@v3 32 | with: 33 | path: ~/go/pkg/mod 34 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 35 | restore-keys: | 36 | ${{ runner.os }}-go- 37 | 38 | - name: Install Dependencies 39 | run: go mod tidy 40 | 41 | - name: Build 42 | run: go build -v ./... 43 | 44 | - name: Run tests 45 | run: go test -v ./tests/... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | pom.xml.tag 3 | pom.xml.releaseBackup 4 | pom.xml.versionsBackup 5 | pom.xml.next 6 | release.properties 7 | dependency-reduced-pom.xml 8 | buildNumber.properties 9 | .mvn/timing.properties 10 | .mvn/wrapper/maven-wrapper.jar 11 | *.iml 12 | logs 13 | .idea 14 | rebel.xml 15 | 16 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iec104 2 | 3 | [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 4 | 5 | English | [中文](README_zh_CN.md) 6 | 7 | IEC104 library 8 | 9 | ## Overview 10 | This project implements a client for the IEC 60870-5-104 protocol (commonly referred to as IEC 104) using the Go programming language. 11 | IEC 104 is a widely used protocol in the electrical and industrial automation sectors, enabling reliable and efficient communication for remote control and data acquisition. 12 | 13 | ## Features 14 | 15 | The library support the following IEC 104 protocol features: 16 | 17 | * TCP/IP Based Communication 18 |
Utilizes standard TCP/IP protocols for communication, ensuring compatibility with a wide range of network infrastructures. 19 | * Multiple Information Types 20 |
Supports transmission of various information types including single point information, double point information, measured values (normalized, scaled, short floating point), integrated totals, and commands (single, double, set point). 21 | * Real-time Data Exchange 22 |
Provides real-time data exchange capabilities, essential for monitoring and controlling industrial processes and electrical systems. 23 | * Time Synchronization 24 |
Supports time synchronization commands to ensure that all connected devices maintain accurate and synchronized time. 25 | * Event-driven Communication 26 |
Supports event-driven data transmission, allowing for efficient communication by only sending updates when changes occur. 27 | * Quality and Priority Indicators 28 |
Includes quality and priority indicators for transmitted data, ensuring that the integrity and importance of the data are maintained. 29 | * Automatic Reconnection 30 |
Implements automatic reconnection mechanisms to handle network disruptions, ensuring continuous and reliable communication. 31 | 32 | ## How to use 33 | ```shell 34 | go get -u github.com/wendy512/iec104 35 | ``` 36 | 37 | - [Server and client test examples](tests/iec104_test.go) 38 | 39 | ## License 40 | iec104 is based on the [Apache License 2.0](./LICENSE) agreement. 41 | ## Contact 42 | 43 | - Email: 44 | -------------------------------------------------------------------------------- /README_zh_CN.md: -------------------------------------------------------------------------------- 1 | # iec104 2 | 3 | [![License](https://img.shields.io/badge/license-Apache--2.0-green.svg)](https://www.apache.org/licenses/LICENSE-2.0.html) 4 | 5 | 中文 | [English](README.md) 6 | 7 | IEC104库 8 | 9 | ## 概述 10 | 该项目实现了一个基于 Go 语言的 IEC 60870-5-104 协议(通常称为 IEC 104)的客户端。 11 | IEC 104 是在电力系统和工业自动化领域广泛使用的协议,旨在实现远程控制和数据采集的可靠、高效通信。 12 | 13 | ## 功能特性 14 | 该库支持以下IEC 104协议功能: 15 | 16 | * 基于 TCP/IP 的通信 17 |
使用标准的 TCP/IP 协议进行通信,确保与各种网络基础设施的兼容性。 18 | * 支持多种信息类型 19 |
支持传输多种信息类型,包括单点信息、双点信息、测量值(规一化值、标度化值、短浮点数)、累积量和命令(单点、双点、设定值)。 20 | * 实时数据交换 21 |
提供实时数据交换功能,适用于监控和控制工业过程和电力系统。 22 | * 时间同步 23 |
支持时间同步命令,确保所有连接设备保持准确和同步的时间。 24 | * 事件驱动的通信 25 |
支持事件驱动的数据传输,仅在变化发生时发送更新,提高通信效率。 26 | * 质量和优先级指示 27 |
包含数据的质量和优先级指示,确保数据的完整性和重要性得到维护。 28 | * 自动重连 29 |
实现了自动重连机制,以处理网络中断,确保连续和可靠的通信。 30 | 31 | ## 如何使用 32 | ```shell 33 | go get -u github.com/wendy512/iec104 34 | ``` 35 | 36 | - [服务端和客户端测试示例](tests/iec104_test.go) 37 | 38 | ## 开源许可 39 | iec104 基于 [Apache License 2.0](./LICENSE) 协议。 40 | 41 | ## 联系方式 42 | 43 | - 邮箱: 44 | -------------------------------------------------------------------------------- /client/core.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/wendy512/iec104/pkg/waitgroup" 7 | "net" 8 | "net/url" 9 | "strconv" 10 | "sync/atomic" 11 | "time" 12 | 13 | "github.com/spf13/cast" 14 | "github.com/wendy512/go-iecp5/asdu" 15 | "github.com/wendy512/go-iecp5/clog" 16 | "github.com/wendy512/go-iecp5/cs104" 17 | ) 18 | 19 | type Client struct { 20 | client104 *cs104.Client 21 | settings *Settings 22 | onConnectHandler func(c *Client) 23 | connectionLostHandler func(c *Client) 24 | } 25 | 26 | // Settings 连接配置 27 | type Settings struct { 28 | Host string 29 | Port int 30 | AutoConnect bool //自动重连 31 | ReconnectInterval time.Duration //重连间隔 32 | Cfg104 *cs104.Config //104协议规范配置 33 | TLS *tls.Config // tls配置 34 | Params *asdu.Params //ASDU相关特定参数 35 | LogCfg *LogCfg 36 | } 37 | 38 | type LogCfg struct { 39 | Enable bool //是否开启log 40 | LogProvider clog.LogProvider 41 | } 42 | 43 | type command struct { 44 | typeId asdu.TypeID 45 | ca asdu.CommonAddr 46 | ioa asdu.InfoObjAddr 47 | t time.Time 48 | qoc asdu.QualifierOfCommand 49 | qos asdu.QualifierOfSetpointCmd 50 | value any 51 | } 52 | 53 | func NewSettings() *Settings { 54 | cfg104 := cs104.DefaultConfig() 55 | return &Settings{ 56 | Host: "localhost", 57 | Port: 2404, 58 | AutoConnect: true, 59 | ReconnectInterval: time.Minute, 60 | Cfg104: &cfg104, 61 | Params: asdu.ParamsWide, 62 | } 63 | } 64 | 65 | func New(settings *Settings, call ASDUCall) *Client { 66 | opts := newClientOption(settings) 67 | handler := &clientHandler{call: call} 68 | client104 := cs104.NewClient(handler, opts) 69 | logCfg := settings.LogCfg 70 | if logCfg != nil { 71 | client104.LogMode(logCfg.Enable) 72 | client104.SetLogProvider(logCfg.LogProvider) 73 | } 74 | 75 | return &Client{ 76 | settings: settings, 77 | client104: client104, 78 | } 79 | } 80 | 81 | func (c *Client) Connect() error { 82 | if err := c.testConnect(); err != nil { 83 | return err 84 | } 85 | 86 | if err := c.client104.Start(); err != nil { 87 | return err 88 | } 89 | 90 | wg := &waitgroup.WaitGroup{} 91 | wg.Add(1) 92 | // 标记是不是第一次 93 | var firstConnect atomic.Bool 94 | // 连接状态事件 95 | c.client104.SetOnConnectHandler(func(cs *cs104.Client) { 96 | if firstConnect.CompareAndSwap(false, true) { 97 | wg.Done() 98 | } 99 | cs.SendStartDt() 100 | if c.onConnectHandler != nil { 101 | c.onConnectHandler(c) 102 | } 103 | }) 104 | 105 | if err := wg.WaitTimeout(c.settings.Cfg104.ConnectTimeout0); err != nil { 106 | return fmt.Errorf("connection timeout of %f seconds", c.settings.Cfg104.ConnectTimeout0.Seconds()) 107 | } 108 | return nil 109 | } 110 | 111 | func (c *Client) Close() error { 112 | c.client104.SendStopDt() 113 | return c.client104.Close() 114 | } 115 | 116 | func (c *Client) SetLogCfg(cfg LogCfg) { 117 | c.client104.LogMode(cfg.Enable) 118 | c.client104.SetLogProvider(cfg.LogProvider) 119 | } 120 | 121 | // SetOnConnectHandler 连接成功后回调,如果连接断开重新连接上也会回调,所以存在多次调用的情况 122 | func (c *Client) SetOnConnectHandler(f func(c *Client)) { 123 | c.onConnectHandler = f 124 | } 125 | 126 | // SetConnectionLostHandler 连接断开后回调,如果连接重复断开也会回调,所以存在多次调用的情况 127 | func (c *Client) SetConnectionLostHandler(f func(c *Client)) { 128 | c.client104.SetConnectionLostHandler(func(_ *cs104.Client) { 129 | f(c) 130 | }) 131 | } 132 | 133 | // SetServerActiveHandler 激活确认后回调,如果连接断开重新连接上也会回调,所以存在多次调用的情况 134 | func (c *Client) SetServerActiveHandler(f func(c *Client)) { 135 | c.client104.SetServerActiveHandler(func(_ *cs104.Client) { 136 | f(c) 137 | }) 138 | } 139 | 140 | func (c *Client) IsConnected() bool { 141 | return c.client104.IsConnected() 142 | } 143 | 144 | // SendCmd 双点遥控 145 | func (c *Client) SendCmd(addr uint16, typeId asdu.TypeID, ioa asdu.InfoObjAddr, value any) error { 146 | cmd := &command{ 147 | typeId: typeId, 148 | ioa: ioa, 149 | ca: asdu.CommonAddr(addr), 150 | value: value, 151 | qoc: asdu.QualifierOfCommand{ 152 | Qual: asdu.QOCNoAdditionalDefinition, 153 | InSelect: false, 154 | }, 155 | qos: asdu.QualifierOfSetpointCmd{ 156 | Qual: 0, 157 | InSelect: false, 158 | }, 159 | t: time.Now(), 160 | } 161 | 162 | return c.doSend(cmd) 163 | } 164 | 165 | // SendInterrogationCmd 发起总召唤 166 | func (c *Client) SendInterrogationCmd(addr uint16) error { 167 | cmd := &command{typeId: asdu.C_IC_NA_1, ca: asdu.CommonAddr(addr)} 168 | return c.doSend(cmd) 169 | } 170 | 171 | // SendClockSynchronizationCmd 发起时钟同步 172 | func (c *Client) SendClockSynchronizationCmd(addr uint16, t time.Time) error { 173 | cmd := &command{typeId: asdu.C_CS_NA_1, ca: asdu.CommonAddr(addr), t: t} 174 | return c.doSend(cmd) 175 | } 176 | 177 | // SendCounterInterrogationCmd 发起累积量召唤 178 | func (c *Client) SendCounterInterrogationCmd(addr uint16) error { 179 | cmd := &command{typeId: asdu.C_CI_NA_1, ca: asdu.CommonAddr(addr)} 180 | return c.doSend(cmd) 181 | } 182 | 183 | // SendReadCmd 发起读命令 184 | func (c *Client) SendReadCmd(addr uint16, ioa uint) error { 185 | cmd := &command{typeId: asdu.C_RD_NA_1, ioa: asdu.InfoObjAddr(ioa), ca: asdu.CommonAddr(addr)} 186 | return c.doSend(cmd) 187 | } 188 | 189 | // SendResetProcessCmd 发起复位进程命令 190 | func (c *Client) SendResetProcessCmd(addr uint16) error { 191 | cmd := &command{typeId: asdu.C_RP_NA_1, ca: asdu.CommonAddr(addr)} 192 | return c.doSend(cmd) 193 | } 194 | 195 | // SendTestCmd 发送带时标的测试命令 196 | func (c *Client) SendTestCmd(addr uint16) error { 197 | cmd := &command{typeId: asdu.C_TS_TA_1, ca: asdu.CommonAddr(addr)} 198 | return c.doSend(cmd) 199 | } 200 | 201 | func (c *Client) doSend(cmd *command) error { 202 | if !c.IsConnected() { 203 | return NotConnected 204 | } 205 | coa := activationCoa() 206 | var err error 207 | 208 | switch cmd.typeId { 209 | case asdu.C_IC_NA_1: 210 | err = c.client104.InterrogationCmd(coa, cmd.ca, asdu.QOIStation) 211 | case asdu.C_CI_NA_1: 212 | qcc := asdu.QualifierCountCall{Request: asdu.QCCTotal, Freeze: asdu.QCCFrzRead} 213 | err = c.client104.CounterInterrogationCmd(coa, cmd.ca, qcc) 214 | case asdu.C_CS_NA_1: 215 | err = c.client104.ClockSynchronizationCmd(coa, cmd.ca, cmd.t) 216 | case asdu.C_RD_NA_1: 217 | err = c.client104.ReadCmd(coa, cmd.ca, cmd.ioa) 218 | case asdu.C_RP_NA_1: 219 | err = c.client104.ResetProcessCmd(coa, cmd.ca, asdu.QPRGeneralRest) 220 | case asdu.C_TS_TA_1: 221 | err = c.client104.TestCommand(coa, cmd.ca) 222 | case asdu.C_SC_NA_1, asdu.C_SC_TA_1: 223 | var value bool 224 | value, err = cast.ToBoolE(cmd.value) 225 | if err != nil { 226 | return err 227 | } 228 | asduCmd := asdu.SingleCommandInfo{ 229 | Ioa: cmd.ioa, 230 | Value: value, 231 | Qoc: cmd.qoc, 232 | } 233 | if cmd.typeId == asdu.C_SC_TA_1 { 234 | asduCmd.Time = cmd.t 235 | } 236 | err = asdu.SingleCmd(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 237 | case asdu.C_DC_NA_1, asdu.C_DC_TA_1: 238 | var value uint8 239 | value, err = cast.ToUint8E(cmd.value) 240 | if err != nil { 241 | return err 242 | } 243 | asduCmd := asdu.DoubleCommandInfo{ 244 | Ioa: cmd.ioa, 245 | Value: asdu.DoubleCommand(value), 246 | Qoc: cmd.qoc, 247 | } 248 | if cmd.typeId == asdu.C_DC_TA_1 { 249 | asduCmd.Time = cmd.t 250 | } 251 | err = asdu.DoubleCmd(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 252 | case asdu.C_RC_NA_1, asdu.C_RC_TA_1: 253 | var value uint8 254 | value, err = cast.ToUint8E(cmd.value) 255 | if err != nil { 256 | return err 257 | } 258 | asduCmd := asdu.StepCommandInfo{ 259 | Ioa: cmd.ioa, 260 | Value: asdu.StepCommand(value), 261 | Qoc: cmd.qoc, 262 | } 263 | if cmd.typeId == asdu.C_RC_TA_1 { 264 | asduCmd.Time = cmd.t 265 | } 266 | err = asdu.StepCmd(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 267 | case asdu.C_SE_NA_1, asdu.C_SE_TA_1: 268 | var value int16 269 | value, err = cast.ToInt16E(cmd.value) 270 | if err != nil { 271 | return err 272 | } 273 | asduCmd := asdu.SetpointCommandNormalInfo{ 274 | Ioa: cmd.ioa, 275 | Value: asdu.Normalize(value), 276 | Qos: cmd.qos, 277 | } 278 | if cmd.typeId == asdu.C_SE_TA_1 { 279 | asduCmd.Time = cmd.t 280 | } 281 | err = asdu.SetpointCmdNormal(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 282 | case asdu.C_SE_NB_1, asdu.C_SE_TB_1: 283 | var value int16 284 | value, err = cast.ToInt16E(cmd.value) 285 | if err != nil { 286 | return err 287 | } 288 | asduCmd := asdu.SetpointCommandScaledInfo{ 289 | Ioa: cmd.ioa, 290 | Value: value, 291 | Qos: cmd.qos, 292 | } 293 | if cmd.typeId == asdu.C_SE_TB_1 { 294 | asduCmd.Time = cmd.t 295 | } 296 | err = asdu.SetpointCmdScaled(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 297 | case asdu.C_SE_NC_1, asdu.C_SE_TC_1: 298 | var value float32 299 | value, err = cast.ToFloat32E(cmd.value) 300 | if err != nil { 301 | return err 302 | } 303 | asduCmd := asdu.SetpointCommandFloatInfo{ 304 | Ioa: cmd.ioa, 305 | Value: value, 306 | Qos: cmd.qos, 307 | } 308 | if cmd.typeId == asdu.C_SE_TC_1 { 309 | asduCmd.Time = cmd.t 310 | } 311 | err = asdu.SetpointCmdFloat(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 312 | case asdu.C_BO_NA_1, asdu.C_BO_TA_1: 313 | var value uint32 314 | value, err = cast.ToUint32E(cmd.value) 315 | if err != nil { 316 | return err 317 | } 318 | asduCmd := asdu.BitsString32CommandInfo{ 319 | Ioa: cmd.ioa, 320 | Value: value, 321 | } 322 | if cmd.typeId == asdu.C_BO_TA_1 { 323 | asduCmd.Time = cmd.t 324 | } 325 | err = asdu.BitsString32Cmd(c.client104, cmd.typeId, coa, cmd.ca, asduCmd) 326 | default: 327 | err = fmt.Errorf("unknow type id %d", cmd.typeId) 328 | } 329 | 330 | return err 331 | } 332 | 333 | func activationCoa() asdu.CauseOfTransmission { 334 | return asdu.CauseOfTransmission{ 335 | IsTest: false, 336 | IsNegative: false, 337 | Cause: asdu.Activation, 338 | } 339 | } 340 | 341 | // testConnect 测试端口连通性 342 | func (c *Client) testConnect() error { 343 | url, _ := url.Parse(formatServerUrl(c.settings)) 344 | var ( 345 | conn net.Conn 346 | err error 347 | ) 348 | 349 | timeout := c.settings.Cfg104.ConnectTimeout0 350 | switch url.Scheme { 351 | case "tcps": 352 | conn, err = tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", url.Host, c.settings.TLS) 353 | default: 354 | conn, err = net.DialTimeout("tcp", url.Host, timeout) 355 | } 356 | 357 | if err != nil { 358 | return err 359 | } 360 | return conn.Close() 361 | } 362 | 363 | func newClientOption(settings *Settings) *cs104.ClientOption { 364 | opts := cs104.NewOption() 365 | if settings.Cfg104 == nil { 366 | opts.SetConfig(cs104.DefaultConfig()) 367 | } else { 368 | opts.SetConfig(*settings.Cfg104) 369 | } 370 | if settings.Params == nil { 371 | opts.SetParams(asdu.ParamsWide) 372 | } else { 373 | opts.SetParams(settings.Params) 374 | } 375 | opts.SetAutoReconnect(settings.AutoConnect) 376 | opts.SetReconnectInterval(settings.ReconnectInterval) 377 | opts.SetTLSConfig(settings.TLS) 378 | 379 | server := formatServerUrl(settings) 380 | _ = opts.AddRemoteServer(server) 381 | return opts 382 | } 383 | 384 | func formatServerUrl(settings *Settings) string { 385 | var server string 386 | if settings.TLS != nil { 387 | server = "tcps://" + settings.Host + ":" + strconv.Itoa(settings.Port) 388 | } else { 389 | server = "tcp://" + settings.Host + ":" + strconv.Itoa(settings.Port) 390 | } 391 | return server 392 | } 393 | -------------------------------------------------------------------------------- /client/errors.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "errors" 4 | 5 | var ( 6 | NotConnected = errors.New("the service request can not be executed because the client is not yet connected") 7 | ) 8 | -------------------------------------------------------------------------------- /client/handle.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/wendy512/go-iecp5/asdu" 4 | 5 | const ( 6 | SinglePoint DataType = iota // 单点信息 7 | DoublePoint // 双点信息 8 | MeasuredValueScaled // 测量值,标度化值信息 9 | MeasuredValueNormal // 测量值,规一化值信息 10 | StepPosition // 步位置信息 11 | BitString32 // 比特位串信息 12 | MeasuredValueFloat // 测量值,短浮点数信息 13 | IntegratedTotals // 累计量信息 14 | EventOfProtectionEquipment // 继电器保护设备事件信息 15 | PackedStartEventsOfProtectionEquipment // 继电器保护设备事件信息 16 | PackedOutputCircuitInfo // 继电器保护设备成组输出电路信息 17 | PackedSinglePointWithSCD // 带变位检出的成组单点信息 18 | SingleCommandInfo 19 | DoubleCommandInfo 20 | StepCommandInfo 21 | SetPointCommandNormalInfo 22 | SetPointCommandScaledInfo 23 | SetPointCommandFloatInfo 24 | BitsString32CommandInfo 25 | UNKNOWN // 未知的 26 | ) 27 | 28 | type DataType int 29 | 30 | type clientHandler struct { 31 | call ASDUCall 32 | } 33 | 34 | // InterrogationHandler 总召唤回复 35 | func (h *clientHandler) InterrogationHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 36 | return h.call.OnInterrogation(rxAsdu) 37 | } 38 | 39 | // CounterInterrogationHandler 总计数器回复 40 | func (h *clientHandler) CounterInterrogationHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 41 | return h.call.OnCounterInterrogation(rxAsdu) 42 | } 43 | 44 | // ReadHandler 读定值回复 45 | func (h *clientHandler) ReadHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 46 | return h.call.OnRead(rxAsdu) 47 | } 48 | 49 | // TestCommandHandler 测试下发回复 50 | func (h *clientHandler) TestCommandHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 51 | return h.call.OnTestCommand(rxAsdu) 52 | } 53 | 54 | // ClockSyncHandler 时钟同步回复 55 | func (h *clientHandler) ClockSyncHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 56 | return h.call.OnClockSync(rxAsdu) 57 | } 58 | 59 | // ResetProcessHandler 进程重置回复 60 | func (h *clientHandler) ResetProcessHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 61 | return h.call.OnResetProcess(rxAsdu) 62 | } 63 | 64 | // DelayAcquisitionHandler 延迟获取回复 65 | func (h *clientHandler) DelayAcquisitionHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 66 | return h.call.OnDelayAcquisition(rxAsdu) 67 | } 68 | 69 | // ASDUHandler ASDU上报,ASDU数据 70 | func (h *clientHandler) ASDUHandler(_ asdu.Connect, rxAsdu *asdu.ASDU) error { 71 | return h.call.OnASDU(rxAsdu) 72 | } 73 | 74 | func GetDataType(typeId asdu.TypeID) DataType { 75 | switch typeId { 76 | case asdu.M_SP_NA_1, asdu.M_SP_TA_1, asdu.M_SP_TB_1: 77 | return SinglePoint 78 | case asdu.M_DP_NA_1, asdu.M_DP_TA_1, asdu.M_DP_TB_1: 79 | return DoublePoint 80 | case asdu.M_ST_NA_1, asdu.M_ST_TA_1, asdu.M_ST_TB_1: 81 | return StepPosition 82 | case asdu.M_BO_NA_1, asdu.M_BO_TA_1, asdu.M_BO_TB_1: 83 | return BitString32 84 | case asdu.M_ME_NB_1, asdu.M_ME_TB_1, asdu.M_ME_TE_1: 85 | return MeasuredValueScaled 86 | case asdu.M_ME_NA_1, asdu.M_ME_TA_1, asdu.M_ME_TD_1, asdu.M_ME_ND_1: 87 | return MeasuredValueNormal 88 | case asdu.M_ME_NC_1, asdu.M_ME_TC_1, asdu.M_ME_TF_1: 89 | return MeasuredValueFloat 90 | case asdu.M_IT_NA_1, asdu.M_IT_TA_1, asdu.M_IT_TB_1: 91 | return IntegratedTotals 92 | case asdu.M_EP_TA_1, asdu.M_EP_TD_1: 93 | return EventOfProtectionEquipment 94 | case asdu.M_EP_TB_1, asdu.M_EP_TE_1: 95 | return PackedStartEventsOfProtectionEquipment 96 | case asdu.M_EP_TC_1, asdu.M_EP_TF_1: 97 | return PackedOutputCircuitInfo 98 | case asdu.M_PS_NA_1: 99 | return PackedSinglePointWithSCD 100 | case asdu.C_SC_NA_1, asdu.C_SC_TA_1: 101 | return SingleCommandInfo 102 | case asdu.C_DC_NA_1, asdu.C_DC_TA_1: 103 | return DoubleCommandInfo 104 | case asdu.C_RC_NA_1, asdu.C_RC_TA_1: 105 | return StepCommandInfo 106 | case asdu.C_SE_NA_1, asdu.C_SE_TA_1: 107 | return SetPointCommandNormalInfo 108 | case asdu.C_SE_NB_1, asdu.C_SE_TB_1: 109 | return SetPointCommandScaledInfo 110 | case asdu.C_SE_NC_1, asdu.C_SE_TC_1: 111 | return SetPointCommandFloatInfo 112 | case asdu.C_BO_NA_1, asdu.C_BO_TA_1: 113 | return BitsString32CommandInfo 114 | default: 115 | return UNKNOWN 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /client/interface.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "github.com/wendy512/go-iecp5/asdu" 4 | 5 | // ASDUCall is the interface of client handler 6 | type ASDUCall interface { 7 | // OnInterrogation 总召唤回复 8 | OnInterrogation(*asdu.ASDU) error 9 | // OnCounterInterrogation 总计数器回复 10 | OnCounterInterrogation(*asdu.ASDU) error 11 | // OnRead 读定值回复 12 | OnRead(*asdu.ASDU) error 13 | // OnTestCommand 测试下发回复 14 | OnTestCommand(*asdu.ASDU) error 15 | // OnClockSync 时钟同步回复 16 | OnClockSync(*asdu.ASDU) error 17 | // OnResetProcess 进程重置回复 18 | OnResetProcess(*asdu.ASDU) error 19 | // OnDelayAcquisition 延迟获取回复 20 | OnDelayAcquisition(*asdu.ASDU) error 21 | // OnASDU 数据回复或控制回复 22 | OnASDU(*asdu.ASDU) error 23 | } 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/wendy512/iec104 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/spf13/cast v1.6.0 7 | github.com/wendy512/go-iecp5 v1.2.3 8 | ) 9 | 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 2 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 3 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 4 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 5 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 6 | github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= 7 | github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= 8 | github.com/wendy512/go-iecp5 v1.2.3 h1:1PCoKzTFmf9Azx2BxoE8k0uH338kUYLnb4PipGHMhXw= 9 | github.com/wendy512/go-iecp5 v1.2.3/go.mod h1:d5koVsFZF0IV2JYznykSLJy6d29mHvXlNqm6GvvJQ0k= 10 | -------------------------------------------------------------------------------- /pkg/waitgroup/waitgroup.go: -------------------------------------------------------------------------------- 1 | package waitgroup 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | // WaitGroup wraps sync.WaitGroup and adds a method WaitTimeout to abort waiting 10 | // for long-running, blocked or leaked goroutines blocking Wait() from the 11 | // underlying WaitGroup. A caller might use this functionality to terminate a 12 | // program in a bounded time. 13 | type WaitGroup struct { 14 | sync.WaitGroup 15 | } 16 | 17 | // ErrTimeout is returned when the timeout in WaitTimeout is exceeded 18 | var ErrTimeout = errors.New("timed out") 19 | 20 | // WaitTimeout blocks until the WaitGroup counter is zero or when timeout is 21 | // exceeded. It spawns an internal goroutine. In case of timeout exceeded the 22 | // error ErrTimeout is returned and the internally spawned goroutine might leak 23 | // if the underlying WaitGroup never returns. 24 | // 25 | // It is safe to call WaitTimeout concurrently but doing so might leak 26 | // additional goroutines as described above. 27 | func (wg *WaitGroup) WaitTimeout(timeout time.Duration) error { 28 | doneCh := make(chan struct{}) 29 | timer := time.NewTimer(timeout) 30 | defer timer.Stop() 31 | 32 | go func() { 33 | wg.Wait() 34 | close(doneCh) 35 | }() 36 | 37 | select { 38 | case <-timer.C: 39 | return ErrTimeout 40 | case <-doneCh: 41 | return nil 42 | } 43 | } 44 | 45 | // Waiter is the interface blocking on Wait(). sync.WaitGroup implements this 46 | // interface. 47 | type Waiter interface { 48 | Wait() 49 | } 50 | 51 | // WaitErrorer is the interface blocking on Wait() and returning any error that 52 | // occurred from Wait(). errgroup.Group implements this interface. 53 | type WaitErrorer interface { 54 | Wait() error 55 | } 56 | 57 | // Await is a convenience function that can be used instead of WaitGroup 58 | // provided by this package. Await blocks until Waiter returns or the specified 59 | // timeout is exceeded. In case of timeout exceeded the error ErrTimeout is 60 | // returned and the internally spawned goroutine might leak if Waiter never returns. 61 | // 62 | // It is safe to call Await concurrently and multiple times but doing so might leak 63 | // additional goroutines as described above. 64 | func Await(wf Waiter, timeout time.Duration) error { 65 | doneCh := make(chan struct{}) 66 | timer := time.NewTimer(timeout) 67 | defer timer.Stop() 68 | 69 | go func() { 70 | wf.Wait() 71 | close(doneCh) 72 | }() 73 | 74 | select { 75 | case <-timer.C: 76 | return ErrTimeout 77 | case <-doneCh: 78 | return nil 79 | } 80 | } 81 | 82 | // AwaitWithError is a convenience function that can be used instead of 83 | // WaitGroup provided by this package. AwaitWithError blocks until WaitErrorer returns 84 | // or the specified timeout is exceeded. Any error from WaitErrorer will be returned 85 | // unless the timeout has been exceeded before. In case of timeout exceeded the 86 | // error ErrTimeout is returned and the internally spawned goroutine might leak 87 | // if WaitErrorer never returns. 88 | // 89 | // It is safe to call AwaitWithError concurrently and multiple times but doing 90 | // so might leak additional goroutines as described above. 91 | func AwaitWithError(we WaitErrorer, timeout time.Duration) error { 92 | doneCh := make(chan struct{}) 93 | errCh := make(chan error) 94 | 95 | timer := time.NewTimer(timeout) 96 | defer timer.Stop() 97 | 98 | go func() { 99 | err := we.Wait() 100 | errCh <- err 101 | close(doneCh) 102 | }() 103 | 104 | select { 105 | case <-timer.C: 106 | return ErrTimeout 107 | case <-doneCh: 108 | return nil 109 | case err := <-errCh: 110 | return err 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /server/core.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/wendy512/go-iecp5/asdu" 7 | "github.com/wendy512/go-iecp5/clog" 8 | "github.com/wendy512/go-iecp5/cs104" 9 | ) 10 | 11 | // Settings 连接配置 12 | type Settings struct { 13 | Host string 14 | Port int 15 | Cfg104 *cs104.Config //104协议规范配置 16 | Params *asdu.Params //ASDU相关特定参数 17 | LogCfg *LogCfg 18 | } 19 | 20 | type LogCfg struct { 21 | Enable bool //是否开启log 22 | LogProvider clog.LogProvider 23 | } 24 | 25 | type Server struct { 26 | settings *Settings 27 | cs104Server *cs104.Server 28 | } 29 | 30 | func NewSettings() *Settings { 31 | cfg104 := cs104.DefaultConfig() 32 | return &Settings{ 33 | Host: "localhost", 34 | Port: 2404, 35 | Cfg104: &cfg104, 36 | Params: asdu.ParamsWide, 37 | } 38 | } 39 | 40 | func New(settings *Settings, handler CommandHandler) *Server { 41 | cs104Server := cs104.NewServer(&serverHandler{h: handler}) 42 | cs104Server.SetConfig(*settings.Cfg104) 43 | cs104Server.SetParams(settings.Params) 44 | 45 | logCfg := settings.LogCfg 46 | if logCfg != nil { 47 | cs104Server.LogMode(logCfg.Enable) 48 | cs104Server.SetLogProvider(logCfg.LogProvider) 49 | } 50 | 51 | return &Server{ 52 | settings: settings, 53 | cs104Server: cs104Server, 54 | } 55 | } 56 | 57 | func (s *Server) Start() { 58 | addr := s.settings.Host + ":" + strconv.Itoa(s.settings.Port) 59 | go s.cs104Server.ListenAndServer(addr) 60 | } 61 | 62 | func (s *Server) Stop() { 63 | _ = s.cs104Server.Close() 64 | } 65 | 66 | // SetOnConnectionHandler set on connect handler 67 | func (s *Server) SetOnConnectionHandler(f func(asdu.Connect)) { 68 | s.cs104Server.SetOnConnectionHandler(f) 69 | } 70 | 71 | // SetConnectionLostHandler set connect lost handler 72 | func (s *Server) SetConnectionLostHandler(f func(asdu.Connect)) { 73 | s.cs104Server.SetConnectionLostHandler(f) 74 | } 75 | -------------------------------------------------------------------------------- /server/handle.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/wendy512/go-iecp5/asdu" 5 | "time" 6 | ) 7 | 8 | type serverHandler struct { 9 | h CommandHandler 10 | } 11 | 12 | func (s *serverHandler) InterrogationHandler(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierOfInterrogation) error { 13 | return s.h.OnInterrogation(conn, pack, quality) 14 | } 15 | 16 | func (s *serverHandler) CounterInterrogationHandler(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierCountCall) error { 17 | return s.h.OnCounterInterrogation(conn, pack, quality) 18 | } 19 | 20 | func (s *serverHandler) ReadHandler(conn asdu.Connect, pack *asdu.ASDU, addr asdu.InfoObjAddr) error { 21 | return s.h.OnRead(conn, pack, addr) 22 | } 23 | 24 | func (s *serverHandler) ClockSyncHandler(conn asdu.Connect, pack *asdu.ASDU, time time.Time) error { 25 | return s.h.OnClockSync(conn, pack, time) 26 | } 27 | 28 | func (s *serverHandler) ResetProcessHandler(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierOfResetProcessCmd) error { 29 | return s.h.OnResetProcess(conn, pack, quality) 30 | } 31 | 32 | func (s *serverHandler) DelayAcquisitionHandler(conn asdu.Connect, pack *asdu.ASDU, msec uint16) error { 33 | return s.h.OnDelayAcquisition(conn, pack, msec) 34 | } 35 | 36 | func (s *serverHandler) ASDUHandler(conn asdu.Connect, pack *asdu.ASDU) error { 37 | return s.h.OnASDU(conn, pack) 38 | } 39 | -------------------------------------------------------------------------------- /server/interface.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "github.com/wendy512/go-iecp5/asdu" 5 | "time" 6 | ) 7 | 8 | type CommandHandler interface { 9 | // OnInterrogation 总召唤请求 10 | OnInterrogation(asdu.Connect, *asdu.ASDU, asdu.QualifierOfInterrogation) error 11 | // OnCounterInterrogation 总计数器请求 12 | OnCounterInterrogation(asdu.Connect, *asdu.ASDU, asdu.QualifierCountCall) error 13 | // OnRead 读定值请求 14 | OnRead(asdu.Connect, *asdu.ASDU, asdu.InfoObjAddr) error 15 | // OnClockSync 时钟同步请求 16 | OnClockSync(asdu.Connect, *asdu.ASDU, time.Time) error 17 | // OnResetProcess 进程重置请求 18 | OnResetProcess(asdu.Connect, *asdu.ASDU, asdu.QualifierOfResetProcessCmd) error 19 | // OnDelayAcquisition 延迟获取请求 20 | OnDelayAcquisition(asdu.Connect, *asdu.ASDU, uint16) error 21 | // OnASDU 控制命令请求 22 | OnASDU(asdu.Connect, *asdu.ASDU) error 23 | } 24 | -------------------------------------------------------------------------------- /tests/client_handle.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wendy512/go-iecp5/asdu" 6 | "github.com/wendy512/iec104/client" 7 | ) 8 | 9 | type clientCall struct { 10 | } 11 | 12 | // OnInterrogation 总召唤回复 13 | func (c *clientCall) OnInterrogation(packet *asdu.ASDU) error { 14 | addr, value := packet.GetInterrogationCmd() 15 | fmt.Printf("interrogation reply, addr: %d, value: %d\n", addr, value) 16 | return nil 17 | } 18 | 19 | // OnCounterInterrogation 总计数器回复 20 | func (c *clientCall) OnCounterInterrogation(packet *asdu.ASDU) error { 21 | addr, value := packet.GetCounterInterrogationCmd() 22 | fmt.Printf("counter interrogation reply, addr: %d, request: 0x%02X, rreeze: 0x%02X\n", 23 | addr, value.Request, value.Freeze) 24 | return nil 25 | } 26 | 27 | // OnRead 读定值回复 28 | func (c *clientCall) OnRead(packet *asdu.ASDU) error { 29 | return c.OnASDU(packet) 30 | } 31 | 32 | // OnTestCommand 测试下发回复 33 | func (c *clientCall) OnTestCommand(packet *asdu.ASDU) error { 34 | addr, value := packet.GetTestCommand() 35 | fmt.Printf("test cmd reply, addr: %d, value: %t\n", addr, value) 36 | return nil 37 | } 38 | 39 | // OnClockSync 时钟同步回复 40 | func (c *clientCall) OnClockSync(packet *asdu.ASDU) error { 41 | addr, value := packet.GetClockSynchronizationCmd() 42 | fmt.Printf("clock sync reply, addr: %d, value: %d\n", addr, value.UnixMilli()) 43 | return nil 44 | } 45 | 46 | // OnResetProcess 进程重置回复 47 | func (c *clientCall) OnResetProcess(packet *asdu.ASDU) error { 48 | addr, value := packet.GetResetProcessCmd() 49 | fmt.Printf("reset process reply, addr: %d, value: 0x%02X\n", addr, value) 50 | return nil 51 | } 52 | 53 | // OnDelayAcquisition 延迟获取回复 54 | func (c *clientCall) OnDelayAcquisition(packet *asdu.ASDU) error { 55 | addr, value := packet.GetDelayAcquireCommand() 56 | fmt.Printf("delay acquisition reply, addr: %d, value: %d\n", addr, value) 57 | return nil 58 | } 59 | 60 | // OnASDU 数据正体 61 | func (c *clientCall) OnASDU(packet *asdu.ASDU) error { 62 | // 读取设备数据 63 | switch client.GetDataType(packet.Type) { 64 | case client.SinglePoint: 65 | c.onSinglePoint(packet) 66 | case client.DoublePoint: 67 | c.onDoublePoint(packet) 68 | case client.MeasuredValueScaled: 69 | c.onMeasuredValueScaled(packet) 70 | case client.MeasuredValueNormal: 71 | c.onMeasuredValueNormal(packet) 72 | case client.StepPosition: 73 | c.onStepPosition(packet) 74 | case client.BitString32: 75 | c.onBitString32(packet) 76 | case client.MeasuredValueFloat: 77 | c.onMeasuredValueFloat(packet) 78 | case client.IntegratedTotals: 79 | c.onIntegratedTotals(packet) 80 | case client.EventOfProtectionEquipment: 81 | c.onEventOfProtectionEquipment(packet) 82 | case client.PackedStartEventsOfProtectionEquipment: 83 | c.onPackedStartEventsOfProtectionEquipment(packet) 84 | case client.PackedOutputCircuitInfo: 85 | c.onPackedOutputCircuitInfo(packet) 86 | case client.PackedSinglePointWithSCD: 87 | c.onPackedSinglePointWithSCD(packet) 88 | default: 89 | return nil 90 | } 91 | 92 | return nil 93 | } 94 | 95 | func (c *clientCall) onSinglePoint(packet *asdu.ASDU) { 96 | // [M_SP_NA_1], [M_SP_TA_1] or [M_SP_TB_1] 获取单点信息信息体集合 97 | for _, p := range packet.GetSinglePoint() { 98 | fmt.Printf("single point, ioa: %d, value: %v\n", p.Ioa, p.Value) 99 | } 100 | } 101 | 102 | func (c *clientCall) onDoublePoint(packet *asdu.ASDU) { 103 | // [M_DP_NA_1], [M_DP_TA_1] or [M_DP_TB_1] 获得双点信息体集合 104 | for _, p := range packet.GetDoublePoint() { 105 | fmt.Printf("double point, ioa: %d, value: %v\n", p.Ioa, p.Value) 106 | } 107 | } 108 | 109 | func (c *clientCall) onMeasuredValueScaled(packet *asdu.ASDU) { 110 | // [M_ME_NB_1], [M_ME_TB_1] or [M_ME_TE_1] 获得测量值,标度化值信息体集合 111 | for _, p := range packet.GetMeasuredValueScaled() { 112 | fmt.Printf("measured value scaled, ioa: %d, value: %v\n", p.Ioa, p.Value) 113 | } 114 | } 115 | 116 | func (c *clientCall) onMeasuredValueNormal(packet *asdu.ASDU) { 117 | // [M_ME_NA_1], [M_ME_TA_1],[ M_ME_TD_1] or [M_ME_ND_1] 获得测量值,规一化值信息体集合 118 | for _, p := range packet.GetMeasuredValueNormal() { 119 | fmt.Printf("measured value normal, ioa: %d, value: %v\n", p.Ioa, p.Value) 120 | } 121 | } 122 | 123 | func (c *clientCall) onStepPosition(packet *asdu.ASDU) { 124 | // [M_ST_NA_1], [M_ST_TA_1] or [M_ST_TB_1] 获得步位置信息体集合 125 | for _, p := range packet.GetStepPosition() { 126 | // state:false: 设备未在瞬变状态 true: 设备处于瞬变状态 127 | fmt.Printf("step position, ioa: %d, state: %t, value: %d\n", p.Ioa, p.Value.HasTransient, p.Value.Val) 128 | } 129 | } 130 | 131 | func (c *clientCall) onBitString32(packet *asdu.ASDU) { 132 | // [M_ME_NC_1], [M_ME_TC_1] or [M_ME_TF_1].获得测量值,短浮点数信息体集合 133 | for _, p := range packet.GetMeasuredValueFloat() { 134 | fmt.Printf("bigtstring32, ioa: %d, value: %v\n", p.Ioa, p.Value) 135 | } 136 | } 137 | 138 | func (c *clientCall) onMeasuredValueFloat(packet *asdu.ASDU) { 139 | // [M_ME_NC_1], [M_ME_TC_1] or [M_ME_TF_1].获得测量值,短浮点数信息体集合 140 | for _, p := range packet.GetMeasuredValueFloat() { 141 | fmt.Printf("measured value float, ioa: %d, value: %v\n", p.Ioa, p.Value) 142 | } 143 | } 144 | 145 | func (c *clientCall) onIntegratedTotals(packet *asdu.ASDU) { 146 | // [M_IT_NA_1], [M_IT_TA_1] or [M_IT_TB_1]. 获得累计量信息体集合 147 | for _, p := range packet.GetIntegratedTotals() { 148 | fmt.Printf("integrated totals, ioa: %d, count: %d, SQ: 0x%02X, CY: %t, CA: %t, IV: %t\n", 149 | p.Ioa, p.Value.CounterReading, p.Value.SeqNumber, p.Value.HasCarry, p.Value.IsAdjusted, p.Value.IsInvalid) 150 | } 151 | } 152 | 153 | func (c *clientCall) onEventOfProtectionEquipment(packet *asdu.ASDU) { 154 | // [M_EP_TA_1] [M_EP_TD_1] 获取继电器保护设备事件信息体 155 | for _, p := range packet.GetEventOfProtectionEquipment() { 156 | fmt.Printf("event of protection equipment, ioa: %d, event: %d, qdp: %d, mesc: %d, time: %d\n", 157 | p.Ioa, p.Event, p.Qdp, p.Msec, p.Time.UnixMilli()) 158 | } 159 | } 160 | 161 | func (c *clientCall) onPackedStartEventsOfProtectionEquipment(packet *asdu.ASDU) { 162 | // [M_EP_TB_1] [M_EP_TE_1] 获取继电器保护设备事件信息体 163 | p := packet.GetPackedStartEventsOfProtectionEquipment() 164 | fmt.Printf("packed start events of protection equipment, ioa: %d, event: %d, qdp: %d, mesc: %d, time: %d\n", 165 | p.Ioa, p.Event, p.Qdp, p.Msec, p.Time.UnixMilli()) 166 | } 167 | 168 | func (c *clientCall) onPackedOutputCircuitInfo(packet *asdu.ASDU) { 169 | // [M_EP_TC_1] [M_EP_TF_1] 获取继电器保护设备成组输出电路信息信息体 170 | p := packet.GetPackedOutputCircuitInfo() 171 | fmt.Printf("packed Output circuit, ioa: %d, qci: %d, qdp: %d, mesc: %d, time: %d\n", 172 | p.Ioa, p.Oci, p.Qdp, p.Msec, p.Time.UnixMilli()) 173 | } 174 | 175 | func (c *clientCall) onPackedSinglePointWithSCD(packet *asdu.ASDU) { 176 | // [M_PS_NA_1]. 获得带变位检出的成组单点信息 177 | for _, p := range packet.GetPackedSinglePointWithSCD() { 178 | fmt.Printf("packed single point with SCD, ioa: %d, scd: %d, qds: %d\n", p.Ioa, p.Scd, p.Qds) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /tests/iec104_test.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "fmt" 5 | "github.com/wendy512/go-iecp5/asdu" 6 | "github.com/wendy512/iec104/client" 7 | "github.com/wendy512/iec104/server" 8 | "sync" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestClient(t *testing.T) { 14 | srv := startServer() 15 | settings := client.NewSettings() 16 | settings.LogCfg = &client.LogCfg{Enable: true} 17 | c := client.New(settings, &clientCall{}) 18 | 19 | wg := sync.WaitGroup{} 20 | wg.Add(1) 21 | c.SetOnConnectHandler(func(c *client.Client) { 22 | // 连接成功以后做的操作 23 | fmt.Printf("connected %s iec104 server\n", settings.Host) 24 | }) 25 | 26 | // server active确认后回调 27 | c.SetServerActiveHandler(func(c *client.Client) { 28 | //// 发送总召唤 29 | if err := c.SendInterrogationCmd(commonAddr); err != nil { 30 | t.Errorf("send interrogation cmd error %v\n", err) 31 | t.FailNow() 32 | } 33 | 34 | // 累积量召唤 35 | if err := c.SendCounterInterrogationCmd(commonAddr); err != nil { 36 | t.Errorf("send counter interrogation cmd error %v\n", err) 37 | t.FailNow() 38 | } 39 | 40 | // read cmd 41 | if err := c.SendReadCmd(commonAddr, 100); err != nil { 42 | t.Errorf("send counter interrogation cmd error %v\n", err) 43 | t.FailNow() 44 | } 45 | 46 | // 时钟同步 47 | if err := c.SendClockSynchronizationCmd(commonAddr, time.Now()); err != nil { 48 | t.Errorf("send clock sync cmd error %v\n", err) 49 | t.FailNow() 50 | } 51 | 52 | // test cmd 53 | if err := c.SendTestCmd(commonAddr); err != nil { 54 | t.Errorf("send test cmd error %v\n", err) 55 | t.FailNow() 56 | } 57 | 58 | // 单点控制 59 | if err := c.SendCmd(commonAddr, asdu.C_SC_NA_1, asdu.InfoObjAddr(1000), false); err != nil { 60 | t.Errorf("send single cmd error %v\n", err) 61 | t.FailNow() 62 | } 63 | 64 | // 测试等待回复,不能结束太快 65 | time.Sleep(time.Second * 10) 66 | wg.Done() 67 | }) 68 | 69 | // Connect后会发送server active 70 | if err := c.Connect(); err != nil { 71 | t.Errorf("client connect error %v\n", err) 72 | t.FailNow() 73 | } 74 | wg.Wait() 75 | 76 | if err := c.Close(); err != nil { 77 | t.Errorf("close error %v\n", err) 78 | t.FailNow() 79 | } 80 | srv.Stop() 81 | } 82 | 83 | func startServer() *server.Server { 84 | s := server.New(server.NewSettings(), &myServerHandler{}) 85 | s.Start() 86 | return s 87 | } 88 | -------------------------------------------------------------------------------- /tests/server_handle.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/wendy512/go-iecp5/asdu" 5 | "time" 6 | ) 7 | 8 | const ( 9 | commonAddr = 1 10 | ) 11 | 12 | type myServerHandler struct { 13 | } 14 | 15 | func (ms *myServerHandler) OnInterrogation(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierOfInterrogation) error { 16 | //_ = pack.SendReplyMirror(conn, asdu.ActivationCon) 17 | // TODO 18 | _ = asdu.Single(conn, false, asdu.CauseOfTransmission{Cause: asdu.InterrogatedByStation}, commonAddr, asdu.SinglePointInfo{ 19 | Ioa: 100, 20 | Value: true, 21 | Qds: asdu.QDSGood, 22 | }) 23 | _ = asdu.Double(conn, false, asdu.CauseOfTransmission{Cause: asdu.InterrogatedByStation}, commonAddr, asdu.DoublePointInfo{ 24 | Ioa: 200, 25 | Value: asdu.DPIDeterminedOn, 26 | Qds: asdu.QDSGood, 27 | }) 28 | //_ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 29 | return nil 30 | } 31 | 32 | func (ms *myServerHandler) OnCounterInterrogation(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierCountCall) error { 33 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 34 | // TODO 35 | _ = asdu.CounterInterrogationCmd(conn, asdu.CauseOfTransmission{Cause: asdu.Activation}, commonAddr, asdu.QualifierCountCall{asdu.QCCGroup1, asdu.QCCFrzRead}) 36 | _ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 37 | return nil 38 | } 39 | 40 | func (ms *myServerHandler) OnRead(conn asdu.Connect, pack *asdu.ASDU, addr asdu.InfoObjAddr) error { 41 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 42 | // TODO 43 | _ = asdu.Single(conn, false, asdu.CauseOfTransmission{Cause: asdu.InterrogatedByStation}, commonAddr, asdu.SinglePointInfo{ 44 | Ioa: addr, 45 | Value: true, 46 | Qds: asdu.QDSGood, 47 | }) 48 | _ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 49 | return nil 50 | } 51 | 52 | func (ms *myServerHandler) OnClockSync(conn asdu.Connect, pack *asdu.ASDU, tm time.Time) error { 53 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 54 | now := time.Now() 55 | _ = asdu.ClockSynchronizationCmd(conn, asdu.CauseOfTransmission{Cause: asdu.Activation}, commonAddr, now) 56 | _ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 57 | return nil 58 | } 59 | 60 | func (ms *myServerHandler) OnResetProcess(conn asdu.Connect, pack *asdu.ASDU, quality asdu.QualifierOfResetProcessCmd) error { 61 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 62 | // TODO 63 | _ = asdu.ResetProcessCmd(conn, asdu.CauseOfTransmission{Cause: asdu.Activation}, commonAddr, asdu.QPRGeneralRest) 64 | _ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 65 | return nil 66 | } 67 | 68 | func (ms *myServerHandler) OnDelayAcquisition(conn asdu.Connect, pack *asdu.ASDU, msec uint16) error { 69 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 70 | // TODO 71 | _ = asdu.DelayAcquireCommand(conn, asdu.CauseOfTransmission{Cause: asdu.Activation}, commonAddr, msec) 72 | _ = pack.SendReplyMirror(conn, asdu.ActivationTerm) 73 | return nil 74 | } 75 | 76 | func (ms *myServerHandler) OnASDU(conn asdu.Connect, pack *asdu.ASDU) error { 77 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 78 | // TODO 79 | cmd := pack.GetSingleCmd() 80 | _ = asdu.SingleCmd(conn, pack.Type, pack.Coa, pack.CommonAddr, asdu.SingleCommandInfo{ 81 | Ioa: cmd.Ioa, 82 | Value: cmd.Value, 83 | Qoc: cmd.Qoc, 84 | }) 85 | _ = pack.SendReplyMirror(conn, asdu.ActivationCon) 86 | return nil 87 | } 88 | --------------------------------------------------------------------------------