├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── _examples ├── cs104_client_general │ └── cliGeneral.go ├── cs104_server_general │ └── srvGeneral.go └── cs104_server_special │ └── srvSepcial.go ├── asdu ├── asdu.go ├── asdu_test.go ├── codec.go ├── cpara.go ├── cpara_test.go ├── cproc.go ├── cproc_test.go ├── csys.go ├── csys_test.go ├── error.go ├── filet.go ├── identifier.go ├── identifier_test.go ├── information.go ├── information_test.go ├── interface.go ├── mproc.go ├── mproc_test.go ├── msys.go ├── msys_test.go ├── time.go └── time_test.go ├── clog └── clog.go ├── cs101 ├── client.go └── ft.go ├── cs104 ├── apci.go ├── apci_test.go ├── client.go ├── clientOption.go ├── common.go ├── config.go ├── error.go ├── interface.go ├── server.go ├── server_session.go └── server_special.go ├── go.mod └── go.sum /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - '**.md' 7 | pull_request: 8 | paths-ignore: 9 | - '**.md' 10 | 11 | env: 12 | GO111MODULE: on 13 | GOPROXY: "https://proxy.golang.org" 14 | 15 | jobs: 16 | build: 17 | name: Test on ${{ matrix.os }} @Go${{ matrix.go-version }} 18 | runs-on: ${{ matrix.os }} 19 | strategy: 20 | matrix: 21 | go-version: ["1.17.x", "1.18.x"] 22 | os: [ubuntu-latest, macos-latest, windows-latest] 23 | 24 | steps: 25 | - name: Set up Go ${{ matrix.go-version }} 26 | uses: actions/setup-go@v3 27 | with: 28 | go-version: ${{ matrix.go-version }} 29 | 30 | - name: Check out code into the Go module directory 31 | uses: actions/checkout@v3 32 | 33 | - name: Print Go environment 34 | id: vars 35 | run: | 36 | printf "Using go at: $(which go)\n" 37 | printf "Go version: $(go version)\n" 38 | printf "\n\nGo environment:\n\n" 39 | go env 40 | printf "\n\nSystem environment:\n\n" 41 | env 42 | # Calculate the short SHA1 hash of the git commit 43 | echo "::set-output name=short_sha::$(git rev-parse --short HEAD)" 44 | echo "::set-output name=go_cache::$(go env GOCACHE)" 45 | 46 | - name: Cache go modules 47 | uses: actions/cache@v3 48 | with: 49 | path: | 50 | ${{ steps.vars.outputs.go_cache }} 51 | ~/go/pkg/mod 52 | key: ${{ runner.os }}-${{ matrix.go-version }}-go-ci-${{ hashFiles('**/go.sum') }} 53 | restore-keys: | 54 | ${{ runner.os }}-${{ matrix.go-version }}-go-ci 55 | 56 | - name: Unit test 57 | run: | 58 | go test -v -race -coverprofile=coverage -covermode=atomic ./... 59 | 60 | - name: Upload coverage to Codecov 61 | uses: codecov/codecov-action@v3 62 | with: 63 | files: ./coverage 64 | flags: unittests 65 | verbose: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-iecp5(Archived) 2 | ## NOTE: Archived, not maintain. 3 | ## NOTE: 已归档, 不再维护, 放弃License. 有需要的可以自由分发 4 | 5 | go-iecp5 library for IEC 60870-5 based protocols in pure go. 6 | The current implementation contains code for IEC 60870-5-104 (protocool over TCP/IP) specifications. 7 | 8 | 9 | 10 | [![Go.Dev reference](https://img.shields.io/badge/go.dev-reference-blue?logo=go&logoColor=white)](https://pkg.go.dev/github.com/thinkgos/go-iecp5?tab=doc) 11 | [![Tests](https://github.com/thinkgos/go-iecp5/actions/workflows/ci.yml/badge.svg)](https://github.com/thinkgos/go-iecp5/actions/workflows/ci.yml) 12 | [![codecov](https://codecov.io/gh/thinkgos/go-iecp5/branch/master/graph/badge.svg)](https://codecov.io/gh/thinkgos/go-iecp5) 13 | [![Go Report Card](https://goreportcard.com/badge/github.com/thinkgos/go-iecp5)](https://goreportcard.com/report/github.com/thinkgos/go-iecp5) 14 | [![License](https://img.shields.io/github/license/thinkgos/go-iecp5)](https://github.com/thinkgos/go-iecp5/raw/master/LICENSE) 15 | [![Tag](https://img.shields.io/github/v/tag/thinkgos/go-iecp5)](https://github.com/thinkgos/go-iecp5/tags) 16 | [![Sourcegraph](https://sourcegraph.com/github.com/thinkgos/go-iecp5/-/badge.svg)](https://sourcegraph.com/github.com/thinkgos/go-iecp5?badge) 17 | 18 | 19 | asdu package: [![GoDoc](https://godoc.org/github.com/thinkgos/go-iecp5/asdu?status.svg)](https://godoc.org/github.com/thinkgos/go-iecp5/asdu) 20 | clog package: [![GoDoc](https://godoc.org/github.com/thinkgos/go-iecp5/clog?status.svg)](https://godoc.org/github.com/thinkgos/go-iecp5/clog) 21 | cs104 package: [![GoDoc](https://godoc.org/github.com/thinkgos/go-iecp5/cs104?status.svg)](https://godoc.org/github.com/thinkgos/go-iecp5/cs104) 22 | 23 | ## Feature: 24 | 25 | - client/server for CS 104 TCP/IP communication 26 | - support for much application layer(except file object) message types, 27 | 28 | # Reference 29 | lib60870 c library [lib60870](https://github.com/mz-automation/lib60870) 30 | lib60870 c library doc [lib60870 doc](https://support.mz-automation.de/doc/lib60870/latest/group__CS104__MASTER.html) 31 | 32 | ## Donation 33 | 34 | if package help you a lot,you can support us by: 35 | 36 | **Alipay** 37 | 38 | ![alipay](https://github.com/thinkgos/thinkgos/blob/master/asserts/alipay.jpg) 39 | 40 | **WeChat Pay** 41 | 42 | ![wxpay](https://github.com/thinkgos/thinkgos/blob/master/asserts/wxpay.jpg) 43 | -------------------------------------------------------------------------------- /_examples/cs104_client_general/cliGeneral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/thinkgos/go-iecp5/asdu" 8 | "github.com/thinkgos/go-iecp5/cs104" 9 | ) 10 | 11 | type myClient struct{} 12 | 13 | func main() { 14 | var err error 15 | 16 | option := cs104.NewOption() 17 | if err = option.AddRemoteServer("127.0.0.1:2404"); err != nil { 18 | panic(err) 19 | } 20 | 21 | mycli := &myClient{} 22 | 23 | client := cs104.NewClient(mycli, option) 24 | 25 | client.LogMode(true) 26 | 27 | client.SetOnConnectHandler(func(c *cs104.Client) { 28 | c.SendStartDt() // 发送startDt激活指令 29 | }) 30 | err = client.Start() 31 | if err != nil { 32 | panic(fmt.Errorf("Failed to connect. error:%v\n", err)) 33 | } 34 | 35 | for { 36 | time.Sleep(time.Second * 100) 37 | } 38 | 39 | } 40 | func (myClient) InterrogationHandler(asdu.Connect, *asdu.ASDU) error { 41 | return nil 42 | } 43 | 44 | func (myClient) CounterInterrogationHandler(asdu.Connect, *asdu.ASDU) error { 45 | return nil 46 | } 47 | func (myClient) ReadHandler(asdu.Connect, *asdu.ASDU) error { 48 | return nil 49 | } 50 | 51 | func (myClient) TestCommandHandler(asdu.Connect, *asdu.ASDU) error { 52 | return nil 53 | } 54 | 55 | func (myClient) ClockSyncHandler(asdu.Connect, *asdu.ASDU) error { 56 | return nil 57 | } 58 | func (myClient) ResetProcessHandler(asdu.Connect, *asdu.ASDU) error { 59 | return nil 60 | } 61 | func (myClient) DelayAcquisitionHandler(asdu.Connect, *asdu.ASDU) error { 62 | return nil 63 | } 64 | func (myClient) ASDUHandler(asdu.Connect, *asdu.ASDU) error { 65 | return nil 66 | } 67 | -------------------------------------------------------------------------------- /_examples/cs104_server_general/srvGeneral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "time" 6 | 7 | "github.com/thinkgos/go-iecp5/asdu" 8 | "github.com/thinkgos/go-iecp5/cs104" 9 | ) 10 | 11 | func main() { 12 | srv := cs104.NewServer(&mysrv{}) 13 | srv.SetOnConnectionHandler(func(c asdu.Connect) { 14 | log.Println("on connect") 15 | }) 16 | srv.SetConnectionLostHandler(func(c asdu.Connect) { 17 | log.Println("connect lost") 18 | }) 19 | srv.LogMode(true) 20 | // go func() { 21 | // time.Sleep(time.Second * 20) 22 | // log.Println("try ooooooo", err) 23 | // err := srv.Close() 24 | // log.Println("ooooooo", err) 25 | // }() 26 | srv.ListenAndServer(":2404") 27 | } 28 | 29 | type mysrv struct{} 30 | 31 | func (sf *mysrv) InterrogationHandler(c asdu.Connect, asduPack *asdu.ASDU, qoi asdu.QualifierOfInterrogation) error { 32 | log.Println("qoi", qoi) 33 | asduPack.SendReplyMirror(c, asdu.ActivationCon) 34 | err := asdu.Single(c, false, asdu.CauseOfTransmission{Cause: asdu.InterrogatedByStation}, asdu.GlobalCommonAddr, 35 | asdu.SinglePointInfo{}) 36 | if err != nil { 37 | // log.Println("falied") 38 | } else { 39 | // log.Println("success") 40 | } 41 | // go func() { 42 | // for { 43 | // err := asdu.Single(c, false, asdu.CauseOfTransmission{Cause: asdu.Spontaneous}, asdu.GlobalCommonAddr, 44 | // asdu.SinglePointInfo{}) 45 | // if err != nil { 46 | // log.Println("falied", err) 47 | // } else { 48 | // log.Println("success", err) 49 | // } 50 | 51 | // time.Sleep(time.Second * 1) 52 | // } 53 | // }() 54 | asduPack.SendReplyMirror(c, asdu.ActivationTerm) 55 | return nil 56 | } 57 | func (sf *mysrv) CounterInterrogationHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierCountCall) error { 58 | return nil 59 | } 60 | func (sf *mysrv) ReadHandler(asdu.Connect, *asdu.ASDU, asdu.InfoObjAddr) error { return nil } 61 | func (sf *mysrv) ClockSyncHandler(asdu.Connect, *asdu.ASDU, time.Time) error { return nil } 62 | func (sf *mysrv) ResetProcessHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierOfResetProcessCmd) error { 63 | return nil 64 | } 65 | func (sf *mysrv) DelayAcquisitionHandler(asdu.Connect, *asdu.ASDU, uint16) error { return nil } 66 | func (sf *mysrv) ASDUHandler(asdu.Connect, *asdu.ASDU) error { return nil } 67 | -------------------------------------------------------------------------------- /_examples/cs104_server_special/srvSepcial.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | 8 | _ "net/http/pprof" 9 | 10 | "github.com/thinkgos/go-iecp5/asdu" 11 | "github.com/thinkgos/go-iecp5/cs104" 12 | ) 13 | 14 | func main() { 15 | option := cs104.NewOption() 16 | err := option.AddRemoteServer("127.0.0.1:2404") 17 | if err != nil { 18 | panic(err) 19 | } 20 | 21 | srv := cs104.NewServerSpecial(&mysrv{}, option) 22 | 23 | srv.LogMode(true) 24 | 25 | srv.SetOnConnectHandler(func(c asdu.Connect) { 26 | _, _ = c.UnderlyingConn().Write([]byte{0x68, 0x0e, 0x00, 0x00, 0x00, 0x00, 0x46, 0x01, 0x04, 0x00, 0xa0, 0xaf, 0xbd, 0xd8, 0x0a, 0xf4}) 27 | log.Println("connected") 28 | }) 29 | srv.SetConnectionLostHandler(func(c asdu.Connect) { 30 | log.Println("disconnected") 31 | }) 32 | if err = srv.Start(); err != nil { 33 | panic(err) 34 | } 35 | 36 | if err := http.ListenAndServe(":6060", nil); err != nil { 37 | panic(err) 38 | } 39 | } 40 | 41 | type mysrv struct{} 42 | 43 | func (sf *mysrv) InterrogationHandler(c asdu.Connect, asduPack *asdu.ASDU, qoi asdu.QualifierOfInterrogation) error { 44 | log.Println("qoi", qoi) 45 | // asduPack.SendReplyMirror(c, asdu.ActivationCon) 46 | // err := asdu.Single(c, false, asdu.CauseOfTransmission{Cause: asdu.Inrogen}, asdu.GlobalCommonAddr, 47 | // asdu.SinglePointInfo{}) 48 | // if err != nil { 49 | // // log.Println("falied") 50 | // } else { 51 | // // log.Println("success") 52 | // } 53 | // asduPack.SendReplyMirror(c, asdu.ActivationTerm) 54 | return nil 55 | } 56 | func (sf *mysrv) CounterInterrogationHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierCountCall) error { 57 | return nil 58 | } 59 | func (sf *mysrv) ReadHandler(asdu.Connect, *asdu.ASDU, asdu.InfoObjAddr) error { return nil } 60 | func (sf *mysrv) ClockSyncHandler(asdu.Connect, *asdu.ASDU, time.Time) error { return nil } 61 | func (sf *mysrv) ResetProcessHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierOfResetProcessCmd) error { 62 | return nil 63 | } 64 | func (sf *mysrv) DelayAcquisitionHandler(asdu.Connect, *asdu.ASDU, uint16) error { return nil } 65 | func (sf *mysrv) ASDUHandler(asdu.Connect, *asdu.ASDU) error { return nil } 66 | -------------------------------------------------------------------------------- /asdu/asdu.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | // Package asdu provides the OSI presentation layer. 6 | package asdu 7 | 8 | import ( 9 | "fmt" 10 | "io" 11 | "math/bits" 12 | "time" 13 | ) 14 | 15 | // ASDUSizeMax asdu max size 16 | const ( 17 | ASDUSizeMax = 249 18 | ) 19 | 20 | // ASDU format 21 | // | data unit identification | information object <1..n> | 22 | // 23 | // | <------------ data unit identification ------------>| 24 | // | typeID | variable struct | cause | common address | 25 | // bytes | 1 | 1 | [1,2] | [1,2] | 26 | // | <------------ information object ------------------>| 27 | // | object address | element set | object time scale | 28 | // bytes | [1,2,3] | | | 29 | 30 | var ( 31 | // ParamsNarrow is the smallest configuration. 32 | ParamsNarrow = &Params{CauseSize: 1, CommonAddrSize: 1, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC} 33 | // ParamsWide is the largest configuration. 34 | ParamsWide = &Params{CauseSize: 2, CommonAddrSize: 2, InfoObjAddrSize: 3, InfoObjTimeZone: time.UTC} 35 | ) 36 | 37 | // Params 定义了ASDU相关特定参数 38 | // See companion standard 101, subclass 7.1. 39 | type Params struct { 40 | // cause of transmission, 传输原因字节数 41 | // The standard requires "b" in [1, 2]. 42 | // Value 2 includes/activates the originator address. 43 | CauseSize int 44 | // Originator Address [1, 255] or 0 for the default. 45 | // The applicability is controlled by Params.CauseSize. 46 | OrigAddress OriginAddr 47 | // size of ASDU common address, ASDU 公共地址字节数 48 | // 应用服务数据单元公共地址的八位位组数目,公共地址是站地址 49 | // The standard requires "a" in [1, 2]. 50 | CommonAddrSize int 51 | 52 | // size of ASDU information object address. 信息对象地址字节数 53 | // The standard requires "c" in [1, 3]. 54 | InfoObjAddrSize int 55 | 56 | // InfoObjTimeZone controls the time tag interpretation. 57 | // The standard fails to mention this one. 58 | InfoObjTimeZone *time.Location 59 | } 60 | 61 | // Valid returns the validation result of params. 62 | func (sf Params) Valid() error { 63 | if (sf.CauseSize < 1 || sf.CauseSize > 2) || 64 | (sf.CommonAddrSize < 1 || sf.CommonAddrSize > 2) || 65 | (sf.InfoObjAddrSize < 1 || sf.InfoObjAddrSize > 3) || 66 | (sf.InfoObjTimeZone == nil) { 67 | return ErrParam 68 | } 69 | return nil 70 | } 71 | 72 | // ValidCommonAddr returns the validation result of a station common address. 73 | func (sf Params) ValidCommonAddr(addr CommonAddr) error { 74 | if addr == InvalidCommonAddr { 75 | return ErrCommonAddrZero 76 | } 77 | if bits.Len(uint(addr)) > sf.CommonAddrSize*8 { 78 | return ErrCommonAddrFit 79 | } 80 | return nil 81 | } 82 | 83 | // IdentifierSize return the application service data unit identifies size 84 | func (sf Params) IdentifierSize() int { 85 | return 2 + int(sf.CauseSize) + int(sf.CommonAddrSize) 86 | } 87 | 88 | // Identifier the application service data unit identifies. 89 | type Identifier struct { 90 | // type identification, information content 91 | Type TypeID 92 | // Variable is variable structure qualifier 93 | Variable VariableStruct 94 | // cause of transmission submission category 95 | Coa CauseOfTransmission 96 | // Originator Address [1, 255] or 0 for the default. 97 | // The applicability is controlled by Params.CauseSize. 98 | OrigAddr OriginAddr 99 | // CommonAddr is a station address. Zero is not used. 100 | // The width is controlled by Params.CommonAddrSize. 101 | // See companion standard 101, subclass 7.2.4. 102 | CommonAddr CommonAddr // station address 公共地址是站地址 103 | } 104 | 105 | // String 返回数据单元标识符的信息,例: "TypeID Cause OrigAddr@CommonAddr" 106 | func (id Identifier) String() string { 107 | if id.OrigAddr == 0 { 108 | return fmt.Sprintf("%s %s @%d", id.Type, id.Coa, id.CommonAddr) 109 | } 110 | return fmt.Sprintf("%s %s %d@%d ", id.Type, id.Coa, id.OrigAddr, id.CommonAddr) 111 | } 112 | 113 | // ASDU (Application Service Data Unit) is an application message. 114 | type ASDU struct { 115 | *Params 116 | Identifier 117 | infoObj []byte // information object serial 118 | bootstrap [ASDUSizeMax]byte // prevents Info malloc 119 | } 120 | 121 | // NewEmptyASDU new empty asdu with special params 122 | func NewEmptyASDU(p *Params) *ASDU { 123 | a := &ASDU{Params: p} 124 | lenDUI := a.IdentifierSize() 125 | a.infoObj = a.bootstrap[lenDUI:lenDUI] 126 | return a 127 | } 128 | 129 | // NewASDU new asdu with special params and identifier 130 | func NewASDU(p *Params, identifier Identifier) *ASDU { 131 | a := NewEmptyASDU(p) 132 | a.Identifier = identifier 133 | return a 134 | } 135 | 136 | // Clone deep clone asdu 137 | func (sf *ASDU) Clone() *ASDU { 138 | r := NewASDU(sf.Params, sf.Identifier) 139 | r.infoObj = append(r.infoObj, sf.infoObj...) 140 | return r 141 | } 142 | 143 | // SetVariableNumber See companion standard 101, subclass 7.2.2. 144 | func (sf *ASDU) SetVariableNumber(n int) error { 145 | if n >= 128 { 146 | return ErrInfoObjIndexFit 147 | } 148 | sf.Variable.Number = byte(n) 149 | return nil 150 | } 151 | 152 | // Respond returns a new "responding" ASDU which addresses "initiating" u. 153 | //func (u *ASDU) Respond(t TypeID, c Cause) *ASDU { 154 | // return NewASDU(u.Params, Identifier{ 155 | // CommonAddr: u.CommonAddr, 156 | // OrigAddr: u.OrigAddr, 157 | // Type: t, 158 | // Cause: c | u.Cause&TestFlag, 159 | // }) 160 | //} 161 | 162 | // Reply returns a new "responding" ASDU which addresses "initiating" addr with a copy of Info. 163 | func (sf *ASDU) Reply(c Cause, addr CommonAddr) *ASDU { 164 | sf.CommonAddr = addr 165 | r := NewASDU(sf.Params, sf.Identifier) 166 | r.Coa.Cause = c 167 | r.infoObj = append(r.infoObj, sf.infoObj...) 168 | return r 169 | } 170 | 171 | // SendReplyMirror send a reply of the mirror request but cause different 172 | func (sf *ASDU) SendReplyMirror(c Connect, cause Cause) error { 173 | r := NewASDU(sf.Params, sf.Identifier) 174 | r.Coa.Cause = cause 175 | r.infoObj = append(r.infoObj, sf.infoObj...) 176 | return c.Send(r) 177 | } 178 | 179 | //// String returns a full description. 180 | //func (u *ASDU) String() string { 181 | // dataSize, err := GetInfoObjSize(u.Type) 182 | // if err != nil { 183 | // if !u.InfoSeq { 184 | // return fmt.Sprintf("%s: %#x", u.Identifier, u.infoObj) 185 | // } 186 | // return fmt.Sprintf("%s seq: %#x", u.Identifier, u.infoObj) 187 | // } 188 | // 189 | // end := len(u.infoObj) 190 | // addrSize := u.InfoObjAddrSize 191 | // if end < addrSize { 192 | // if !u.InfoSeq { 193 | // return fmt.Sprintf("%s: %#x ", u.Identifier, u.infoObj) 194 | // } 195 | // return fmt.Sprintf("%s seq: %#x ", u.Identifier, u.infoObj) 196 | // } 197 | // addr := u.ParseInfoObjAddr(u.infoObj) 198 | // 199 | // buf := bytes.NewBufferString(u.Identifier.String()) 200 | // 201 | // for i := addrSize; ; { 202 | // start := i 203 | // i += dataSize 204 | // if i > end { 205 | // fmt.Fprintf(buf, " %d:%#x ", addr, u.infoObj[start:]) 206 | // break 207 | // } 208 | // fmt.Fprintf(buf, " %d:%#x", addr, u.infoObj[start:i]) 209 | // if i == end { 210 | // break 211 | // } 212 | // 213 | // if u.InfoSeq { 214 | // addr++ 215 | // } else { 216 | // start = i 217 | // i += addrSize 218 | // if i > end { 219 | // fmt.Fprintf(buf, " %#x ", u.infoObj[start:i]) 220 | // break 221 | // } 222 | // addr = u.ParseInfoObjAddr(u.infoObj[start:]) 223 | // } 224 | // } 225 | // 226 | // return buf.String() 227 | //} 228 | 229 | // MarshalBinary honors the encoding.BinaryMarshaler interface. 230 | func (sf *ASDU) MarshalBinary() (data []byte, err error) { 231 | switch { 232 | case sf.Coa.Cause == Unused: 233 | return nil, ErrCauseZero 234 | case !(sf.CauseSize == 1 || sf.CauseSize == 2): 235 | return nil, ErrParam 236 | case sf.CauseSize == 1 && sf.OrigAddr != 0: 237 | return nil, ErrOriginAddrFit 238 | case sf.CommonAddr == InvalidCommonAddr: 239 | return nil, ErrCommonAddrZero 240 | case !(sf.CommonAddrSize == 1 || sf.CommonAddrSize == 2): 241 | return nil, ErrParam 242 | case sf.CommonAddrSize == 1 && sf.CommonAddr != GlobalCommonAddr && sf.CommonAddr >= 255: 243 | return nil, ErrParam 244 | } 245 | 246 | raw := sf.bootstrap[:(sf.IdentifierSize() + len(sf.infoObj))] 247 | raw[0] = byte(sf.Type) 248 | raw[1] = sf.Variable.Value() 249 | raw[2] = sf.Coa.Value() 250 | offset := 3 251 | if sf.CauseSize == 2 { 252 | raw[offset] = byte(sf.OrigAddr) 253 | offset++ 254 | } 255 | if sf.CommonAddrSize == 1 { 256 | if sf.CommonAddr == GlobalCommonAddr { 257 | raw[offset] = 255 258 | } else { 259 | raw[offset] = byte(sf.CommonAddr) 260 | } 261 | } else { // 2 262 | raw[offset] = byte(sf.CommonAddr) 263 | offset++ 264 | raw[offset] = byte(sf.CommonAddr >> 8) 265 | } 266 | return raw, nil 267 | } 268 | 269 | // UnmarshalBinary honors the encoding.BinaryUnmarshaler interface. 270 | // ASDUParams must be set in advance. All other fields are initialized. 271 | func (sf *ASDU) UnmarshalBinary(rawAsdu []byte) error { 272 | if !(sf.CauseSize == 1 || sf.CauseSize == 2) || 273 | !(sf.CommonAddrSize == 1 || sf.CommonAddrSize == 2) { 274 | return ErrParam 275 | } 276 | 277 | // rawAsdu unit identifier size check 278 | lenDUI := sf.IdentifierSize() 279 | if lenDUI > len(rawAsdu) { 280 | return io.EOF 281 | } 282 | 283 | // parse rawAsdu unit identifier 284 | sf.Type = TypeID(rawAsdu[0]) 285 | sf.Variable = ParseVariableStruct(rawAsdu[1]) 286 | sf.Coa = ParseCauseOfTransmission(rawAsdu[2]) 287 | if sf.CauseSize == 1 { 288 | sf.OrigAddr = 0 289 | } else { 290 | sf.OrigAddr = OriginAddr(rawAsdu[3]) 291 | } 292 | if sf.CommonAddrSize == 1 { 293 | sf.CommonAddr = CommonAddr(rawAsdu[lenDUI-1]) 294 | if sf.CommonAddr == 255 { // map 8-bit variant to 16-bit equivalent 295 | sf.CommonAddr = GlobalCommonAddr 296 | } 297 | } else { // 2 298 | sf.CommonAddr = CommonAddr(rawAsdu[lenDUI-2]) | CommonAddr(rawAsdu[lenDUI-1])<<8 299 | } 300 | // information object 301 | sf.infoObj = append(sf.bootstrap[lenDUI:lenDUI], rawAsdu[lenDUI:]...) 302 | return sf.fixInfoObjSize() 303 | } 304 | 305 | // fixInfoObjSize fix information object size 306 | func (sf *ASDU) fixInfoObjSize() error { 307 | // fixed element size 308 | objSize, err := GetInfoObjSize(sf.Type) 309 | if err != nil { 310 | return err 311 | } 312 | 313 | var size int 314 | // read the variable structure qualifier 315 | if sf.Variable.IsSequence { 316 | size = sf.InfoObjAddrSize + int(sf.Variable.Number)*objSize 317 | } else { 318 | size = int(sf.Variable.Number) * (sf.InfoObjAddrSize + objSize) 319 | } 320 | 321 | switch { 322 | case size == 0: 323 | return ErrInfoObjIndexFit 324 | case size > len(sf.infoObj): 325 | return io.EOF 326 | case size < len(sf.infoObj): // not explicitly prohibited 327 | sf.infoObj = sf.infoObj[:size] 328 | } 329 | 330 | return nil 331 | } 332 | -------------------------------------------------------------------------------- /asdu/asdu_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestParams_Valid(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | this *Params 13 | wantErr bool 14 | }{ 15 | {"invalid", &Params{}, true}, 16 | {"ParamsNarrow", ParamsNarrow, false}, 17 | {"ParamsWide", ParamsWide, false}, 18 | } 19 | for _, tt := range tests { 20 | t.Run(tt.name, func(t *testing.T) { 21 | if err := tt.this.Valid(); (err != nil) != tt.wantErr { 22 | t.Errorf("Params.Valid() error = %v, wantErr %v", err, tt.wantErr) 23 | } 24 | }) 25 | } 26 | } 27 | 28 | func TestParams_ValidCommonAddr(t *testing.T) { 29 | type args struct { 30 | addr CommonAddr 31 | } 32 | tests := []struct { 33 | name string 34 | this *Params 35 | args args 36 | wantErr bool 37 | }{ 38 | {"common address zero", ParamsNarrow, args{InvalidCommonAddr}, true}, 39 | {"common address size(1),invalid", ParamsNarrow, args{256}, true}, 40 | {"common address size(1),valid", ParamsNarrow, args{255}, false}, 41 | {"common address size(2),valid", ParamsWide, args{65535}, false}, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | if err := tt.this.ValidCommonAddr(tt.args.addr); (err != nil) != tt.wantErr { 46 | t.Errorf("Params.ValidCommonAddr() error = %v, wantErr %v", err, tt.wantErr) 47 | } 48 | }) 49 | } 50 | } 51 | 52 | func TestParams_IdentifierSize(t *testing.T) { 53 | tests := []struct { 54 | name string 55 | this *Params 56 | want int 57 | }{ 58 | {"ParamsNarrow(4)", ParamsNarrow, 4}, 59 | {"ParamsWide(6)", ParamsWide, 6}, 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | if got := tt.this.IdentifierSize(); got != tt.want { 64 | t.Errorf("Params.IdentifierSize() = %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestASDU_SetVariableNumber(t *testing.T) { 71 | type fields struct { 72 | Params *Params 73 | Identifier Identifier 74 | InfoObj []byte 75 | bootstrap [ASDUSizeMax]byte 76 | } 77 | type args struct { 78 | n int 79 | } 80 | tests := []struct { 81 | name string 82 | fields fields 83 | args args 84 | wantErr bool 85 | }{ 86 | // TODO: Add test cases. 87 | } 88 | for _, tt := range tests { 89 | t.Run(tt.name, func(t *testing.T) { 90 | this := &ASDU{ 91 | Params: tt.fields.Params, 92 | Identifier: tt.fields.Identifier, 93 | infoObj: tt.fields.InfoObj, 94 | bootstrap: tt.fields.bootstrap, 95 | } 96 | if err := this.SetVariableNumber(tt.args.n); (err != nil) != tt.wantErr { 97 | t.Errorf("ASDU.SetVariableNumber() error = %v, wantErr %v", err, tt.wantErr) 98 | } 99 | }) 100 | } 101 | } 102 | 103 | func TestASDU_Reply(t *testing.T) { 104 | type fields struct { 105 | Params *Params 106 | Identifier Identifier 107 | InfoObj []byte 108 | bootstrap [ASDUSizeMax]byte 109 | } 110 | type args struct { 111 | c Cause 112 | addr CommonAddr 113 | } 114 | tests := []struct { 115 | name string 116 | fields fields 117 | args args 118 | want *ASDU 119 | }{ 120 | // TODO: Add test cases. 121 | } 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | this := &ASDU{ 125 | Params: tt.fields.Params, 126 | Identifier: tt.fields.Identifier, 127 | infoObj: tt.fields.InfoObj, 128 | bootstrap: tt.fields.bootstrap, 129 | } 130 | if got := this.Reply(tt.args.c, tt.args.addr); !reflect.DeepEqual(got, tt.want) { 131 | t.Errorf("ASDU.Reply() = %v, want %v", got, tt.want) 132 | } 133 | }) 134 | } 135 | } 136 | 137 | func TestASDU_MarshalBinary(t *testing.T) { 138 | type fields struct { 139 | Params *Params 140 | Identifier Identifier 141 | InfoObj []byte 142 | } 143 | tests := []struct { 144 | name string 145 | fields fields 146 | wantData []byte 147 | wantErr bool 148 | }{ 149 | { 150 | "unused cause", 151 | fields{ 152 | ParamsNarrow, 153 | Identifier{ 154 | M_SP_NA_1, 155 | VariableStruct{}, 156 | CauseOfTransmission{Cause: Unused}, 157 | 0, 158 | 0x80}, 159 | nil}, 160 | nil, 161 | true, 162 | }, 163 | { 164 | "invalid cause size", 165 | fields{ 166 | &Params{CauseSize: 0, CommonAddrSize: 1, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC}, 167 | Identifier{ 168 | M_SP_NA_1, 169 | VariableStruct{}, 170 | CauseOfTransmission{Cause: Activation}, 171 | 0, 172 | 0x80}, 173 | nil}, 174 | nil, 175 | true, 176 | }, 177 | { 178 | "cause size(1),but origAddress not equal zero", 179 | fields{ 180 | &Params{CauseSize: 1, CommonAddrSize: 1, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC}, 181 | Identifier{ 182 | M_SP_NA_1, 183 | VariableStruct{}, 184 | CauseOfTransmission{Cause: Activation}, 185 | 1, 186 | 0x80}, 187 | nil}, 188 | nil, 189 | true, 190 | }, 191 | { 192 | "invalid common address", 193 | fields{ 194 | &Params{CauseSize: 1, CommonAddrSize: 1, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC}, 195 | Identifier{ 196 | M_SP_NA_1, 197 | VariableStruct{}, 198 | CauseOfTransmission{Cause: Activation}, 199 | 0, 200 | InvalidCommonAddr}, 201 | nil}, 202 | nil, 203 | true}, 204 | { 205 | "invalid common address size", 206 | fields{ 207 | &Params{CauseSize: 1, CommonAddrSize: 0, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC}, 208 | Identifier{ 209 | M_SP_NA_1, 210 | VariableStruct{}, 211 | CauseOfTransmission{Cause: Activation}, 212 | 0, 213 | 0x80}, 214 | nil}, 215 | nil, 216 | true, 217 | }, 218 | { 219 | "common size(1),but common address equal 255", 220 | fields{ 221 | &Params{CauseSize: 1, CommonAddrSize: 1, InfoObjAddrSize: 1, InfoObjTimeZone: time.UTC}, 222 | Identifier{ 223 | M_SP_NA_1, 224 | VariableStruct{}, 225 | CauseOfTransmission{Cause: Activation}, 226 | 0, 227 | 255}, 228 | nil}, 229 | nil, 230 | true, 231 | }, 232 | { 233 | "ParamsNarrow", 234 | fields{ 235 | ParamsNarrow, 236 | Identifier{ 237 | M_SP_NA_1, 238 | VariableStruct{Number: 1}, 239 | CauseOfTransmission{Cause: Activation}, 240 | 0, 241 | 0x80}, 242 | []byte{0x00, 0x01, 0x02, 0x03}}, 243 | []byte{0x01, 0x01, 0x06, 0x80, 0x00, 0x01, 0x02, 0x03}, 244 | false, 245 | }, 246 | { 247 | "ParamsNarrow global address", 248 | fields{ 249 | ParamsNarrow, 250 | Identifier{ 251 | M_SP_NA_1, 252 | VariableStruct{Number: 1}, 253 | CauseOfTransmission{Cause: Activation}, 254 | 0, 255 | GlobalCommonAddr}, 256 | []byte{0x00, 0x01, 0x02, 0x03}}, 257 | []byte{0x01, 0x01, 0x06, 0xff, 0x00, 0x01, 0x02, 0x03}, 258 | false, 259 | }, 260 | { 261 | "ParamsWide", 262 | fields{ 263 | ParamsWide, 264 | Identifier{ 265 | M_SP_NA_1, 266 | VariableStruct{Number: 1}, 267 | CauseOfTransmission{Cause: Activation}, 268 | 0, 269 | 0x6080}, 270 | []byte{0x00, 0x01, 0x02, 0x03}}, 271 | []byte{0x01, 0x01, 0x06, 0x00, 0x80, 0x60, 0x00, 0x01, 0x02, 0x03}, 272 | false, 273 | }, 274 | } 275 | for _, tt := range tests { 276 | t.Run(tt.name, func(t *testing.T) { 277 | this := NewASDU(tt.fields.Params, tt.fields.Identifier) 278 | this.infoObj = append(this.infoObj, tt.fields.InfoObj...) 279 | 280 | gotData, err := this.MarshalBinary() 281 | if (err != nil) != tt.wantErr { 282 | t.Errorf("ASDU.MarshalBinary() error = %v, wantErr %v", err, tt.wantErr) 283 | return 284 | } 285 | if !reflect.DeepEqual(gotData, tt.wantData) { 286 | t.Errorf("ASDU.MarshalBinary() = % x, want % x", gotData, tt.wantData) 287 | } 288 | }) 289 | } 290 | } 291 | 292 | func TestASDU_UnmarshalBinary(t *testing.T) { 293 | type args struct { 294 | data []byte 295 | } 296 | tests := []struct { 297 | name string 298 | Params *Params 299 | args args 300 | want []byte 301 | wantErr bool 302 | }{ 303 | { 304 | "invalid param", 305 | &Params{}, 306 | args{}, // 125 307 | []byte{}, 308 | true, 309 | }, 310 | { 311 | "less than data unit identifier size", 312 | ParamsWide, 313 | args{[]byte{0x0b, 0x01, 0x06, 0x80}}, 314 | []byte{}, 315 | true, 316 | }, 317 | { 318 | "type id fix size error", 319 | ParamsWide, 320 | args{[]byte{0x07d, 0x01, 0x06, 0x00, 0x80, 0x60}}, 321 | []byte{}, 322 | true, 323 | }, 324 | 325 | { 326 | "ParamsNarrow global address", 327 | ParamsNarrow, 328 | args{[]byte{0x0b, 0x01, 0x06, 0x80, 0x00, 0x01, 0x02, 0x03}}, 329 | []byte{0x00, 0x01, 0x02, 0x03}, 330 | false, 331 | }, 332 | { 333 | "ParamsNarrow", 334 | ParamsNarrow, 335 | args{[]byte{0x0b, 0x01, 0x06, 0xff, 0x00, 0x01, 0x02, 0x03}}, 336 | []byte{0x00, 0x01, 0x02, 0x03}, 337 | false, 338 | }, 339 | { 340 | "ParamsWide", 341 | ParamsWide, 342 | args{[]byte{0x01, 0x01, 0x06, 0x00, 0x80, 0x60, 0x00, 0x01, 0x02, 0x03}}, 343 | []byte{0x00, 0x01, 0x02, 0x03}, 344 | false, 345 | }, 346 | { 347 | "ParamsWide sequence", 348 | ParamsWide, 349 | args{[]byte{0x01, 0x81, 0x06, 0x00, 0x80, 0x60, 0x00, 0x01, 0x02, 0x03}}, 350 | []byte{0x00, 0x01, 0x02, 0x03}, 351 | false, 352 | }, 353 | } 354 | for _, tt := range tests { 355 | t.Run(tt.name, func(t *testing.T) { 356 | this := NewEmptyASDU(tt.Params) 357 | if err := this.UnmarshalBinary(tt.args.data); (err != nil) != tt.wantErr { 358 | t.Errorf("ASDU.UnmarshalBinary() error = %v, wantErr %v", err, tt.wantErr) 359 | } 360 | if !reflect.DeepEqual(this.infoObj, tt.want) { 361 | t.Errorf("ASDU.UnmarshalBinary() got % x, want % x", this.infoObj, tt.want) 362 | } 363 | }) 364 | } 365 | } 366 | -------------------------------------------------------------------------------- /asdu/codec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "encoding/binary" 9 | "math" 10 | "time" 11 | ) 12 | 13 | // AppendBytes append some bytes to info object 14 | func (sf *ASDU) AppendBytes(b ...byte) *ASDU { 15 | sf.infoObj = append(sf.infoObj, b...) 16 | return sf 17 | } 18 | 19 | // DecodeByte decode a byte then the pass it 20 | func (sf *ASDU) DecodeByte() byte { 21 | v := sf.infoObj[0] 22 | sf.infoObj = sf.infoObj[1:] 23 | return v 24 | } 25 | 26 | // AppendUint16 append some uint16 to info object 27 | func (sf *ASDU) AppendUint16(b uint16) *ASDU { 28 | sf.infoObj = append(sf.infoObj, byte(b&0xff), byte((b>>8)&0xff)) 29 | return sf 30 | } 31 | 32 | // DecodeUint16 decode a uint16 then the pass it 33 | func (sf *ASDU) DecodeUint16() uint16 { 34 | v := binary.LittleEndian.Uint16(sf.infoObj) 35 | sf.infoObj = sf.infoObj[2:] 36 | return v 37 | } 38 | 39 | // AppendInfoObjAddr append information object address to information object 40 | func (sf *ASDU) AppendInfoObjAddr(addr InfoObjAddr) error { 41 | switch sf.InfoObjAddrSize { 42 | case 1: 43 | if addr > 255 { 44 | return ErrInfoObjAddrFit 45 | } 46 | sf.infoObj = append(sf.infoObj, byte(addr)) 47 | case 2: 48 | if addr > 65535 { 49 | return ErrInfoObjAddrFit 50 | } 51 | sf.infoObj = append(sf.infoObj, byte(addr), byte(addr>>8)) 52 | case 3: 53 | if addr > 16777215 { 54 | return ErrInfoObjAddrFit 55 | } 56 | sf.infoObj = append(sf.infoObj, byte(addr), byte(addr>>8), byte(addr>>16)) 57 | default: 58 | return ErrParam 59 | } 60 | return nil 61 | } 62 | 63 | // DecodeInfoObjAddr decode info object address then the pass it 64 | func (sf *ASDU) DecodeInfoObjAddr() InfoObjAddr { 65 | var ioa InfoObjAddr 66 | switch sf.InfoObjAddrSize { 67 | case 1: 68 | ioa = InfoObjAddr(sf.infoObj[0]) 69 | sf.infoObj = sf.infoObj[1:] 70 | case 2: 71 | ioa = InfoObjAddr(sf.infoObj[0]) | (InfoObjAddr(sf.infoObj[1]) << 8) 72 | sf.infoObj = sf.infoObj[2:] 73 | case 3: 74 | ioa = InfoObjAddr(sf.infoObj[0]) | (InfoObjAddr(sf.infoObj[1]) << 8) | (InfoObjAddr(sf.infoObj[2]) << 16) 75 | sf.infoObj = sf.infoObj[3:] 76 | default: 77 | panic(ErrParam) 78 | } 79 | return ioa 80 | } 81 | 82 | // AppendNormalize append a Normalize value to info object 83 | func (sf *ASDU) AppendNormalize(n Normalize) *ASDU { 84 | sf.infoObj = append(sf.infoObj, byte(n), byte(n>>8)) 85 | return sf 86 | } 87 | 88 | // DecodeNormalize decode info object byte to a Normalize value 89 | func (sf *ASDU) DecodeNormalize() Normalize { 90 | n := Normalize(binary.LittleEndian.Uint16(sf.infoObj)) 91 | sf.infoObj = sf.infoObj[2:] 92 | return n 93 | } 94 | 95 | // AppendScaled append a Scaled value to info object 96 | // See companion standard 101, subclass 7.2.6.7. 97 | func (sf *ASDU) AppendScaled(i int16) *ASDU { 98 | sf.infoObj = append(sf.infoObj, byte(i), byte(i>>8)) 99 | return sf 100 | } 101 | 102 | // DecodeScaled decode info object byte to a Scaled value 103 | func (sf *ASDU) DecodeScaled() int16 { 104 | s := int16(binary.LittleEndian.Uint16(sf.infoObj)) 105 | sf.infoObj = sf.infoObj[2:] 106 | return s 107 | } 108 | 109 | // AppendFloat32 append a float32 value to info object 110 | // See companion standard 101, subclass 7.2.6.8. 111 | func (sf *ASDU) AppendFloat32(f float32) *ASDU { 112 | bits := math.Float32bits(f) 113 | sf.infoObj = append(sf.infoObj, byte(bits), byte(bits>>8), byte(bits>>16), byte(bits>>24)) 114 | return sf 115 | } 116 | 117 | // DecodeFloat32 decode info object byte to a float32 value 118 | func (sf *ASDU) DecodeFloat32() float32 { 119 | f := math.Float32frombits(binary.LittleEndian.Uint32(sf.infoObj)) 120 | sf.infoObj = sf.infoObj[4:] 121 | return f 122 | } 123 | 124 | // AppendBinaryCounterReading append binary couter reading value to info object 125 | // See companion standard 101, subclass 7.2.6.9. 126 | func (sf *ASDU) AppendBinaryCounterReading(v BinaryCounterReading) *ASDU { 127 | value := v.SeqNumber & 0x1f 128 | if v.HasCarry { 129 | value |= 0x20 130 | } 131 | if v.IsAdjusted { 132 | value |= 0x40 133 | } 134 | if v.IsInvalid { 135 | value |= 0x80 136 | } 137 | sf.infoObj = append(sf.infoObj, byte(v.CounterReading), byte(v.CounterReading>>8), 138 | byte(v.CounterReading>>16), byte(v.CounterReading>>24), value) 139 | return sf 140 | } 141 | 142 | // DecodeBinaryCounterReading decode info object byte to binary couter reading value 143 | func (sf *ASDU) DecodeBinaryCounterReading() BinaryCounterReading { 144 | v := int32(binary.LittleEndian.Uint32(sf.infoObj)) 145 | b := sf.infoObj[4] 146 | sf.infoObj = sf.infoObj[5:] 147 | return BinaryCounterReading{ 148 | v, 149 | b & 0x1f, 150 | b&0x20 == 0x20, 151 | b&0x40 == 0x40, 152 | b&0x80 == 0x80, 153 | } 154 | } 155 | 156 | // AppendBitsString32 append a bits string value to info object 157 | // See companion standard 101, subclass 7.2.6.13. 158 | func (sf *ASDU) AppendBitsString32(v uint32) *ASDU { 159 | sf.infoObj = append(sf.infoObj, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)) 160 | return sf 161 | } 162 | 163 | // DecodeBitsString32 decode info object byte to a bits string value 164 | func (sf *ASDU) DecodeBitsString32() uint32 { 165 | v := binary.LittleEndian.Uint32(sf.infoObj) 166 | sf.infoObj = sf.infoObj[4:] 167 | return v 168 | } 169 | 170 | // AppendCP56Time2a append a CP56Time2a value to info object 171 | func (sf *ASDU) AppendCP56Time2a(t time.Time, loc *time.Location) *ASDU { 172 | sf.infoObj = append(sf.infoObj, CP56Time2a(t, loc)...) 173 | return sf 174 | } 175 | 176 | // DecodeCP56Time2a decode info object byte to CP56Time2a 177 | func (sf *ASDU) DecodeCP56Time2a() time.Time { 178 | t := ParseCP56Time2a(sf.infoObj, sf.InfoObjTimeZone) 179 | sf.infoObj = sf.infoObj[7:] 180 | return t 181 | } 182 | 183 | // AppendCP24Time2a append CP24Time2a to asdu info object 184 | func (sf *ASDU) AppendCP24Time2a(t time.Time, loc *time.Location) *ASDU { 185 | sf.infoObj = append(sf.infoObj, CP24Time2a(t, loc)...) 186 | return sf 187 | } 188 | 189 | // DecodeCP24Time2a decode info object byte to CP24Time2a 190 | func (sf *ASDU) DecodeCP24Time2a() time.Time { 191 | t := ParseCP24Time2a(sf.infoObj, sf.Params.InfoObjTimeZone) 192 | sf.infoObj = sf.infoObj[3:] 193 | return t 194 | } 195 | 196 | // AppendCP16Time2a append CP16Time2a to asdu info object 197 | func (sf *ASDU) AppendCP16Time2a(msec uint16) *ASDU { 198 | sf.infoObj = append(sf.infoObj, CP16Time2a(msec)...) 199 | return sf 200 | } 201 | 202 | // DecodeCP16Time2a decode info object byte to CP16Time2a 203 | func (sf *ASDU) DecodeCP16Time2a() uint16 { 204 | t := ParseCP16Time2a(sf.infoObj) 205 | sf.infoObj = sf.infoObj[2:] 206 | return t 207 | } 208 | 209 | // AppendStatusAndStatusChangeDetection append StatusAndStatusChangeDetection value to asdu info object 210 | func (sf *ASDU) AppendStatusAndStatusChangeDetection(scd StatusAndStatusChangeDetection) *ASDU { 211 | sf.infoObj = append(sf.infoObj, byte(scd), byte(scd>>8), byte(scd>>16), byte(scd>>24)) 212 | return sf 213 | } 214 | 215 | // DecodeStatusAndStatusChangeDetection decode info object byte to StatusAndStatusChangeDetection 216 | func (sf *ASDU) DecodeStatusAndStatusChangeDetection() StatusAndStatusChangeDetection { 217 | s := StatusAndStatusChangeDetection(binary.LittleEndian.Uint32(sf.infoObj)) 218 | sf.infoObj = sf.infoObj[4:] 219 | return s 220 | } 221 | -------------------------------------------------------------------------------- /asdu/cpara.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | // 在控制方向参数的应用服务数据单元 8 | 9 | // ParameterNormalInfo 测量值参数,归一化值 信息体 10 | type ParameterNormalInfo struct { 11 | Ioa InfoObjAddr 12 | Value Normalize 13 | Qpm QualifierOfParameterMV 14 | } 15 | 16 | // ParameterNormal 测量值参数,规一化值, 只有单个信息对象(SQ = 0) 17 | // [P_ME_NA_1], See companion standard 101, subclass 7.3.5.1 18 | // 传送原因(coa)用于 19 | // 控制方向: 20 | // <6> := 激活 21 | // 监视方向: 22 | // <7> := 激活确认 23 | // <20> := 响应站召唤 24 | // <21> := 响应第 1 组召唤 25 | // <22> := 响应第 2 组召唤 26 | // 至 27 | // <36> := 响应第 16 组召唤 28 | // <44> := 未知的类型标识 29 | // <45> := 未知的传送原因 30 | // <46> := 未知的应用服务数据单元公共地址 31 | // <47> := 未知的信息对象地址 32 | func ParameterNormal(c Connect, coa CauseOfTransmission, ca CommonAddr, p ParameterNormalInfo) error { 33 | if coa.Cause != Activation { 34 | return ErrCmdCause 35 | } 36 | if err := c.Params().Valid(); err != nil { 37 | return err 38 | } 39 | 40 | u := NewASDU(c.Params(), Identifier{ 41 | P_ME_NA_1, 42 | VariableStruct{IsSequence: false, Number: 1}, 43 | coa, 44 | 0, 45 | ca, 46 | }) 47 | if err := u.AppendInfoObjAddr(p.Ioa); err != nil { 48 | return err 49 | } 50 | u.AppendNormalize(p.Value) 51 | u.AppendBytes(p.Qpm.Value()) 52 | return c.Send(u) 53 | } 54 | 55 | // ParameterScaledInfo 测量值参数,标度化值 信息体 56 | type ParameterScaledInfo struct { 57 | Ioa InfoObjAddr 58 | Value int16 59 | Qpm QualifierOfParameterMV 60 | } 61 | 62 | // ParameterScaled 测量值参数,标度化值, 只有单个信息对象(SQ = 0) 63 | // [P_ME_NB_1], See companion standard 101, subclass 7.3.5.2 64 | // 传送原因(coa)用于 65 | // 控制方向: 66 | // <6> := 激活 67 | // 监视方向: 68 | // <7> := 激活确认 69 | // <20> := 响应站召唤 70 | // <21> := 响应第 1 组召唤 71 | // <22> := 响应第 2 组召唤 72 | // 至 73 | // <36> := 响应第 16 组召唤 74 | // <44> := 未知的类型标识 75 | // <45> := 未知的传送原因 76 | // <46> := 未知的应用服务数据单元公共地址 77 | // <47> := 未知的信息对象地址 78 | func ParameterScaled(c Connect, coa CauseOfTransmission, ca CommonAddr, p ParameterScaledInfo) error { 79 | if coa.Cause != Activation { 80 | return ErrCmdCause 81 | } 82 | if err := c.Params().Valid(); err != nil { 83 | return err 84 | } 85 | 86 | u := NewASDU(c.Params(), Identifier{ 87 | P_ME_NB_1, 88 | VariableStruct{IsSequence: false, Number: 1}, 89 | coa, 90 | 0, 91 | ca, 92 | }) 93 | if err := u.AppendInfoObjAddr(p.Ioa); err != nil { 94 | return err 95 | } 96 | u.AppendScaled(p.Value).AppendBytes(p.Qpm.Value()) 97 | return c.Send(u) 98 | } 99 | 100 | // ParameterFloatInfo 测量参数,短浮点数 信息体 101 | type ParameterFloatInfo struct { 102 | Ioa InfoObjAddr 103 | Value float32 104 | Qpm QualifierOfParameterMV 105 | } 106 | 107 | // ParameterFloat 测量值参数,短浮点数, 只有单个信息对象(SQ = 0) 108 | // [P_ME_NC_1], See companion standard 101, subclass 7.3.5.3 109 | // 传送原因(coa)用于 110 | // 控制方向: 111 | // <6> := 激活 112 | // 监视方向: 113 | // <7> := 激活确认 114 | // <20> := 响应站召唤 115 | // <21> := 响应第 1 组召唤 116 | // <22> := 响应第 2 组召唤 117 | // 至 118 | // <36> := 响应第 16 组召唤 119 | // <44> := 未知的类型标识 120 | // <45> := 未知的传送原因 121 | // <46> := 未知的应用服务数据单元公共地址 122 | // <47> := 未知的信息对象地址 123 | func ParameterFloat(c Connect, coa CauseOfTransmission, ca CommonAddr, p ParameterFloatInfo) error { 124 | if coa.Cause != Activation { 125 | return ErrCmdCause 126 | } 127 | if err := c.Params().Valid(); err != nil { 128 | return err 129 | } 130 | 131 | u := NewASDU(c.Params(), Identifier{ 132 | P_ME_NC_1, 133 | VariableStruct{IsSequence: false, Number: 1}, 134 | coa, 135 | 0, 136 | ca, 137 | }) 138 | if err := u.AppendInfoObjAddr(p.Ioa); err != nil { 139 | return err 140 | } 141 | u.AppendFloat32(p.Value).AppendBytes(p.Qpm.Value()) 142 | return c.Send(u) 143 | } 144 | 145 | // ParameterActivationInfo 参数激活 信息体 146 | type ParameterActivationInfo struct { 147 | Ioa InfoObjAddr 148 | Qpa QualifierOfParameterAct 149 | } 150 | 151 | // ParameterActivation 参数激活, 只有单个信息对象(SQ = 0) 152 | // [P_AC_NA_1], See companion standard 101, subclass 7.3.5.4 153 | // 传送原因(coa)用于 154 | // 控制方向: 155 | // <6> := 激活 156 | // <8> := 停止激活 157 | // 监视方向: 158 | // <7> := 激活确认 159 | // <9> := 停止激活确认 160 | // <44> := 未知的类型标识 161 | // <45> := 未知的传送原因 162 | // <46> := 未知的应用服务数据单元公共地址 163 | // <47> := 未知的信息对象地址 164 | func ParameterActivation(c Connect, coa CauseOfTransmission, ca CommonAddr, p ParameterActivationInfo) error { 165 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 166 | return ErrCmdCause 167 | } 168 | if err := c.Params().Valid(); err != nil { 169 | return err 170 | } 171 | 172 | u := NewASDU(c.Params(), Identifier{ 173 | P_AC_NA_1, 174 | VariableStruct{IsSequence: false, Number: 1}, 175 | coa, 176 | 0, 177 | ca, 178 | }) 179 | if err := u.AppendInfoObjAddr(p.Ioa); err != nil { 180 | return err 181 | } 182 | u.AppendBytes(byte(p.Qpa)) 183 | return c.Send(u) 184 | } 185 | 186 | // GetParameterNormal [P_ME_NA_1],获取 测量值参数,标度化值 信息体 187 | func (sf *ASDU) GetParameterNormal() ParameterNormalInfo { 188 | return ParameterNormalInfo{ 189 | sf.DecodeInfoObjAddr(), 190 | sf.DecodeNormalize(), 191 | ParseQualifierOfParamMV(sf.infoObj[0]), 192 | } 193 | } 194 | 195 | // GetParameterScaled [P_ME_NB_1],获取 测量值参数,归一化值 信息体 196 | func (sf *ASDU) GetParameterScaled() ParameterScaledInfo { 197 | return ParameterScaledInfo{ 198 | sf.DecodeInfoObjAddr(), 199 | sf.DecodeScaled(), 200 | ParseQualifierOfParamMV(sf.infoObj[0]), 201 | } 202 | } 203 | 204 | // GetParameterFloat [P_ME_NC_1],获取 测量值参数,短浮点数 信息体 205 | func (sf *ASDU) GetParameterFloat() ParameterFloatInfo { 206 | return ParameterFloatInfo{ 207 | sf.DecodeInfoObjAddr(), 208 | sf.DecodeFloat32(), 209 | ParseQualifierOfParamMV(sf.infoObj[0]), 210 | } 211 | } 212 | 213 | // GetParameterActivation [P_AC_NA_1],获取 参数激活 信息体 214 | func (sf *ASDU) GetParameterActivation() ParameterActivationInfo { 215 | return ParameterActivationInfo{ 216 | sf.DecodeInfoObjAddr(), 217 | QualifierOfParameterAct(sf.infoObj[0]), 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /asdu/cpara_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestParameterNormal(t *testing.T) { 10 | type args struct { 11 | c Connect 12 | coa CauseOfTransmission 13 | ca CommonAddr 14 | p ParameterNormalInfo 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantErr bool 20 | }{ 21 | { 22 | "cause not act", 23 | args{ 24 | newConn(nil, t), 25 | CauseOfTransmission{Cause: Unused}, 26 | 0x1234, 27 | ParameterNormalInfo{ 28 | 0x567890, 29 | 0x3344, 30 | QualifierOfParameterMV{}}}, 31 | true, 32 | }, 33 | { 34 | "P_ME_NA_1", 35 | args{ 36 | newConn([]byte{byte(P_ME_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 37 | 0x90, 0x78, 0x56, 0x44, 0x33, 0x01}, t), 38 | CauseOfTransmission{Cause: Activation}, 39 | 0x1234, 40 | ParameterNormalInfo{ 41 | 0x567890, 42 | 0x3344, 43 | QualifierOfParameterMV{ 44 | QPMThreshold, 45 | false, 46 | false}}}, 47 | false, 48 | }, 49 | } 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | if err := ParameterNormal(tt.args.c, tt.args.coa, tt.args.ca, tt.args.p); (err != nil) != tt.wantErr { 53 | t.Errorf("ParameterNormal() error = %v, wantErr %v", err, tt.wantErr) 54 | } 55 | }) 56 | } 57 | } 58 | 59 | func TestParameterScaled(t *testing.T) { 60 | type args struct { 61 | c Connect 62 | coa CauseOfTransmission 63 | ca CommonAddr 64 | p ParameterScaledInfo 65 | } 66 | tests := []struct { 67 | name string 68 | args args 69 | wantErr bool 70 | }{ 71 | { 72 | "cause not act", 73 | args{ 74 | newConn(nil, t), 75 | CauseOfTransmission{Cause: Unused}, 76 | 0x1234, 77 | ParameterScaledInfo{ 78 | 0x567890, 79 | 0x3344, 80 | QualifierOfParameterMV{}}}, 81 | true, 82 | }, 83 | { 84 | "P_ME_NB_1", 85 | args{ 86 | newConn([]byte{byte(P_ME_NB_1), 0x01, 0x06, 0x00, 0x34, 0x12, 87 | 0x90, 0x78, 0x56, 0x44, 0x33, 0x01}, t), 88 | CauseOfTransmission{Cause: Activation}, 89 | 0x1234, 90 | ParameterScaledInfo{ 91 | 0x567890, 92 | 0x3344, 93 | QualifierOfParameterMV{ 94 | QPMThreshold, 95 | false, 96 | false}}}, 97 | false, 98 | }, 99 | } 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | if err := ParameterScaled(tt.args.c, tt.args.coa, tt.args.ca, tt.args.p); (err != nil) != tt.wantErr { 103 | t.Errorf("ParameterScaled() error = %v, wantErr %v", err, tt.wantErr) 104 | } 105 | }) 106 | } 107 | } 108 | 109 | func TestParameterFloat(t *testing.T) { 110 | bits := math.Float32bits(100) 111 | 112 | type args struct { 113 | c Connect 114 | coa CauseOfTransmission 115 | ca CommonAddr 116 | p ParameterFloatInfo 117 | } 118 | tests := []struct { 119 | name string 120 | args args 121 | wantErr bool 122 | }{ 123 | { 124 | "cause not act", 125 | args{ 126 | newConn(nil, t), 127 | CauseOfTransmission{Cause: Unused}, 128 | 0x1234, 129 | ParameterFloatInfo{ 130 | 0x567890, 131 | 100, 132 | QualifierOfParameterMV{}}}, 133 | true, 134 | }, 135 | { 136 | "P_ME_NC_1", 137 | args{ 138 | newConn([]byte{byte(P_ME_NC_1), 0x01, 0x06, 0x00, 0x34, 0x12, 139 | 0x90, 0x78, 0x56, byte(bits), byte(bits >> 8), byte(bits >> 16), byte(bits >> 24), 0x01}, t), 140 | CauseOfTransmission{Cause: Activation}, 141 | 0x1234, 142 | ParameterFloatInfo{ 143 | 0x567890, 144 | 100, 145 | QualifierOfParameterMV{ 146 | QPMThreshold, 147 | false, 148 | false}}}, 149 | false, 150 | }, 151 | } 152 | for _, tt := range tests { 153 | t.Run(tt.name, func(t *testing.T) { 154 | if err := ParameterFloat(tt.args.c, tt.args.coa, tt.args.ca, tt.args.p); (err != nil) != tt.wantErr { 155 | t.Errorf("ParameterFloat() error = %v, wantErr %v", err, tt.wantErr) 156 | } 157 | }) 158 | } 159 | } 160 | 161 | func TestParameterActivation(t *testing.T) { 162 | type args struct { 163 | c Connect 164 | coa CauseOfTransmission 165 | ca CommonAddr 166 | p ParameterActivationInfo 167 | } 168 | tests := []struct { 169 | name string 170 | args args 171 | wantErr bool 172 | }{ 173 | { 174 | "cause not act and deact", 175 | args{ 176 | newConn(nil, t), 177 | CauseOfTransmission{Cause: Unused}, 178 | 0x1234, 179 | ParameterActivationInfo{ 180 | 0x567890, 181 | QPAUnused}}, 182 | true, 183 | }, 184 | { 185 | "P_AC_NA_1", 186 | args{ 187 | newConn([]byte{byte(P_AC_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 188 | 0x90, 0x78, 0x56, 0x00}, t), 189 | CauseOfTransmission{Cause: Activation}, 190 | 0x1234, 191 | ParameterActivationInfo{ 192 | 0x567890, 193 | QPAUnused}}, 194 | false, 195 | }, 196 | } 197 | for _, tt := range tests { 198 | t.Run(tt.name, func(t *testing.T) { 199 | if err := ParameterActivation(tt.args.c, tt.args.coa, tt.args.ca, tt.args.p); (err != nil) != tt.wantErr { 200 | t.Errorf("ParameterActivation() error = %v, wantErr %v", err, tt.wantErr) 201 | } 202 | }) 203 | } 204 | } 205 | 206 | func TestASDU_GetParameterNormal(t *testing.T) { 207 | type fields struct { 208 | Params *Params 209 | infoObj []byte 210 | } 211 | tests := []struct { 212 | name string 213 | fields fields 214 | want ParameterNormalInfo 215 | }{ 216 | { 217 | "P_ME_NA_1", 218 | fields{ 219 | ParamsWide, 220 | []byte{0x90, 0x78, 0x56, 0x44, 0x33, 0x01}}, 221 | ParameterNormalInfo{ 222 | 0x567890, 223 | 0x3344, 224 | QualifierOfParameterMV{ 225 | QPMThreshold, 226 | false, 227 | false}}, 228 | }, 229 | } 230 | for _, tt := range tests { 231 | t.Run(tt.name, func(t *testing.T) { 232 | this := &ASDU{ 233 | Params: tt.fields.Params, 234 | infoObj: tt.fields.infoObj, 235 | } 236 | if got := this.GetParameterNormal(); !reflect.DeepEqual(got, tt.want) { 237 | t.Errorf("ASDU.GetParameterNormal() = %v, want %v", got, tt.want) 238 | } 239 | }) 240 | } 241 | } 242 | 243 | func TestASDU_GetParameterScaled(t *testing.T) { 244 | type fields struct { 245 | Params *Params 246 | infoObj []byte 247 | } 248 | tests := []struct { 249 | name string 250 | fields fields 251 | want ParameterScaledInfo 252 | }{ 253 | { 254 | "P_ME_NB_1", 255 | fields{ 256 | ParamsWide, 257 | []byte{0x90, 0x78, 0x56, 0x44, 0x33, 0x01}}, 258 | ParameterScaledInfo{ 259 | 0x567890, 260 | 0x3344, 261 | QualifierOfParameterMV{ 262 | QPMThreshold, 263 | false, 264 | false}}, 265 | }, 266 | } 267 | for _, tt := range tests { 268 | t.Run(tt.name, func(t *testing.T) { 269 | this := &ASDU{ 270 | Params: tt.fields.Params, 271 | infoObj: tt.fields.infoObj, 272 | } 273 | if got := this.GetParameterScaled(); !reflect.DeepEqual(got, tt.want) { 274 | t.Errorf("ASDU.GetParameterScaled() = %v, want %v", got, tt.want) 275 | } 276 | }) 277 | } 278 | } 279 | 280 | func TestASDU_GetParameterFloat(t *testing.T) { 281 | bits := math.Float32bits(100) 282 | 283 | type fields struct { 284 | Params *Params 285 | infoObj []byte 286 | } 287 | tests := []struct { 288 | name string 289 | fields fields 290 | want ParameterFloatInfo 291 | }{ 292 | { 293 | "P_ME_NC_1", 294 | fields{ 295 | ParamsWide, 296 | []byte{0x90, 0x78, 0x56, byte(bits), byte(bits >> 8), byte(bits >> 16), byte(bits >> 24), 0x01}}, 297 | ParameterFloatInfo{ 298 | 0x567890, 299 | 100, 300 | QualifierOfParameterMV{ 301 | QPMThreshold, 302 | false, 303 | false}}, 304 | }, 305 | } 306 | for _, tt := range tests { 307 | t.Run(tt.name, func(t *testing.T) { 308 | this := &ASDU{ 309 | Params: tt.fields.Params, 310 | infoObj: tt.fields.infoObj, 311 | } 312 | if got := this.GetParameterFloat(); !reflect.DeepEqual(got, tt.want) { 313 | t.Errorf("ASDU.GetParameterFloat() = %v, want %v", got, tt.want) 314 | } 315 | }) 316 | } 317 | } 318 | 319 | func TestASDU_GetParameterActivation(t *testing.T) { 320 | type fields struct { 321 | Params *Params 322 | infoObj []byte 323 | } 324 | tests := []struct { 325 | name string 326 | fields fields 327 | want ParameterActivationInfo 328 | }{ 329 | { 330 | "P_AC_NA_1", 331 | fields{ 332 | ParamsWide, 333 | []byte{0x90, 0x78, 0x56, 0x00}}, 334 | ParameterActivationInfo{ 335 | 0x567890, 336 | QPAUnused}, 337 | }, 338 | } 339 | for _, tt := range tests { 340 | t.Run(tt.name, func(t *testing.T) { 341 | this := &ASDU{ 342 | Params: tt.fields.Params, 343 | infoObj: tt.fields.infoObj, 344 | } 345 | if got := this.GetParameterActivation(); !reflect.DeepEqual(got, tt.want) { 346 | t.Errorf("ASDU.GetParameterActivation() = %v, want %v", got, tt.want) 347 | } 348 | }) 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /asdu/cproc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | // 在控制方向过程信息的应用服务数据单元 12 | 13 | // SingleCommandInfo 单命令 信息体 14 | type SingleCommandInfo struct { 15 | Ioa InfoObjAddr 16 | Value bool 17 | Qoc QualifierOfCommand 18 | Time time.Time 19 | } 20 | 21 | // SingleCmd sends a type identification [C_SC_NA_1] or [C_SC_TA_1]. 单命令, 只有单个信息对象(SQ = 0) 22 | // [C_SC_NA_1] See companion standard 101, subclass 7.3.2.1 23 | // [C_SC_TA_1] See companion standard 101, 24 | // 传送原因(coa)用于 25 | // 控制方向: 26 | // <6> := 激活 27 | // <8> := 停止激活 28 | // 监视方向: 29 | // <7> := 激活确认 30 | // <9> := 停止激活确认 31 | // <10> := 激活终止 32 | // <44> := 未知的类型标识 33 | // <45> := 未知的传送原因 34 | // <46> := 未知的应用服务数据单元公共地址 35 | // <47> := 未知的信息对象地址 36 | func SingleCmd(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, cmd SingleCommandInfo) error { 37 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 38 | return ErrCmdCause 39 | } 40 | if err := c.Params().Valid(); err != nil { 41 | return err 42 | } 43 | 44 | u := NewASDU(c.Params(), Identifier{ 45 | typeID, 46 | VariableStruct{IsSequence: false, Number: 1}, 47 | coa, 48 | 0, 49 | ca, 50 | }) 51 | 52 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 53 | return err 54 | } 55 | value := cmd.Qoc.Value() 56 | if cmd.Value { 57 | value |= 0x01 58 | } 59 | u.AppendBytes(value) 60 | switch typeID { 61 | case C_SC_NA_1: 62 | case C_SC_TA_1: 63 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 64 | default: 65 | return ErrTypeIDNotMatch 66 | } 67 | return c.Send(u) 68 | } 69 | 70 | // DoubleCommandInfo 单命令 信息体 71 | type DoubleCommandInfo struct { 72 | Ioa InfoObjAddr 73 | Value DoubleCommand 74 | Qoc QualifierOfCommand 75 | Time time.Time 76 | } 77 | 78 | // DoubleCmd sends a type identification [C_DC_NA_1] or [C_DC_TA_1]. 双命令, 只有单个信息对象(SQ = 0) 79 | // [C_DC_NA_1] See companion standard 101, subclass 7.3.2.2 80 | // [C_DC_TA_1] See companion standard 101, 81 | // 传送原因(coa)用于 82 | // 控制方向: 83 | // <6> := 激活 84 | // <8> := 停止激活 85 | // 监视方向: 86 | // <7> := 激活确认 87 | // <9> := 停止激活确认 88 | // <10> := 激活终止 89 | // <44> := 未知的类型标识 90 | // <45> := 未知的传送原因 91 | // <46> := 未知的应用服务数据单元公共地址 92 | // <47> := 未知的信息对象地址 93 | func DoubleCmd(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, 94 | cmd DoubleCommandInfo) error { 95 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 96 | return ErrCmdCause 97 | } 98 | if err := c.Params().Valid(); err != nil { 99 | return err 100 | } 101 | u := NewASDU(c.Params(), Identifier{ 102 | typeID, 103 | VariableStruct{IsSequence: false, Number: 1}, 104 | coa, 105 | 0, 106 | ca, 107 | }) 108 | 109 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 110 | return err 111 | } 112 | 113 | u.AppendBytes(cmd.Qoc.Value() | byte(cmd.Value&0x03)) 114 | switch typeID { 115 | case C_DC_NA_1: 116 | case C_DC_TA_1: 117 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 118 | default: 119 | return ErrTypeIDNotMatch 120 | } 121 | return c.Send(u) 122 | } 123 | 124 | // StepCommandInfo 步调节 信息体 125 | type StepCommandInfo struct { 126 | Ioa InfoObjAddr 127 | Value StepCommand 128 | Qoc QualifierOfCommand 129 | Time time.Time 130 | } 131 | 132 | // StepCmd sends a type [C_RC_NA_1] or [C_RC_TA_1]. 步调节命令, 只有单个信息对象(SQ = 0) 133 | // [C_RC_NA_1] See companion standard 101, subclass 7.3.2.3 134 | // [C_RC_TA_1] See companion standard 101, 135 | // 传送原因(coa)用于 136 | // 控制方向: 137 | // <6> := 激活 138 | // <8> := 停止激活 139 | // 监视方向: 140 | // <7> := 激活确认 141 | // <9> := 停止激活确认 142 | // <10> := 激活终止 143 | // <44> := 未知的类型标识 144 | // <45> := 未知的传送原因 145 | // <46> := 未知的应用服务数据单元公共地址 146 | // <47> := 未知的信息对象地址 147 | func StepCmd(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, cmd StepCommandInfo) error { 148 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 149 | return ErrCmdCause 150 | } 151 | if err := c.Params().Valid(); err != nil { 152 | return err 153 | } 154 | u := NewASDU(c.Params(), Identifier{ 155 | typeID, 156 | VariableStruct{IsSequence: false, Number: 1}, 157 | coa, 158 | 0, 159 | ca, 160 | }) 161 | 162 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 163 | return err 164 | } 165 | 166 | u.AppendBytes(cmd.Qoc.Value() | byte(cmd.Value&0x03)) 167 | switch typeID { 168 | case C_RC_NA_1: 169 | case C_RC_TA_1: 170 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 171 | default: 172 | return ErrTypeIDNotMatch 173 | } 174 | return c.Send(u) 175 | } 176 | 177 | // SetpointCommandNormalInfo 设置命令,规一化值 信息体 178 | type SetpointCommandNormalInfo struct { 179 | Ioa InfoObjAddr 180 | Value Normalize 181 | Qos QualifierOfSetpointCmd 182 | Time time.Time 183 | } 184 | 185 | // SetpointCmdNormal sends a type [C_SE_NA_1] or [C_SE_TA_1]. 设定命令,规一化值, 只有单个信息对象(SQ = 0) 186 | // [C_SE_NA_1] See companion standard 101, subclass 7.3.2.4 187 | // [C_SE_TA_1] See companion standard 101, 188 | // 传送原因(coa)用于 189 | // 控制方向: 190 | // <6> := 激活 191 | // <8> := 停止激活 192 | // 监视方向: 193 | // <7> := 激活确认 194 | // <9> := 停止激活确认 195 | // <10> := 激活终止 196 | // <44> := 未知的类型标识 197 | // <45> := 未知的传送原因 198 | // <46> := 未知的应用服务数据单元公共地址 199 | // <47> := 未知的信息对象地址 200 | func SetpointCmdNormal(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, cmd SetpointCommandNormalInfo) error { 201 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 202 | return ErrCmdCause 203 | } 204 | if err := c.Params().Valid(); err != nil { 205 | return err 206 | } 207 | u := NewASDU(c.Params(), Identifier{ 208 | typeID, 209 | VariableStruct{IsSequence: false, Number: 1}, 210 | coa, 211 | 0, 212 | ca, 213 | }) 214 | 215 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 216 | return err 217 | } 218 | u.AppendNormalize(cmd.Value).AppendBytes(cmd.Qos.Value()) 219 | switch typeID { 220 | case C_SE_NA_1: 221 | case C_SE_TA_1: 222 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 223 | default: 224 | return ErrTypeIDNotMatch 225 | } 226 | return c.Send(u) 227 | } 228 | 229 | // SetpointCommandScaledInfo 设定命令,标度化值 信息体 230 | type SetpointCommandScaledInfo struct { 231 | Ioa InfoObjAddr 232 | Value int16 233 | Qos QualifierOfSetpointCmd 234 | Time time.Time 235 | } 236 | 237 | // SetpointCmdScaled sends a type [C_SE_NB_1] or [C_SE_TB_1]. 设定命令,标度化值,只有单个信息对象(SQ = 0) 238 | // [C_SE_NB_1] See companion standard 101, subclass 7.3.2.5 239 | // [C_SE_TB_1] See companion standard 101, 240 | // 传送原因(coa)用于 241 | // 控制方向: 242 | // <6> := 激活 243 | // <8> := 停止激活 244 | // 监视方向: 245 | // <7> := 激活确认 246 | // <9> := 停止激活确认 247 | // <10> := 激活终止 248 | // <44> := 未知的类型标识 249 | // <45> := 未知的传送原因 250 | // <46> := 未知的应用服务数据单元公共地址 251 | // <47> := 未知的信息对象地址 252 | func SetpointCmdScaled(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, cmd SetpointCommandScaledInfo) error { 253 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 254 | return ErrCmdCause 255 | } 256 | if err := c.Params().Valid(); err != nil { 257 | return err 258 | } 259 | u := NewASDU(c.Params(), Identifier{ 260 | typeID, 261 | VariableStruct{IsSequence: false, Number: 1}, 262 | coa, 263 | 0, 264 | ca, 265 | }) 266 | 267 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 268 | return err 269 | } 270 | u.AppendScaled(cmd.Value).AppendBytes(cmd.Qos.Value()) 271 | switch typeID { 272 | case C_SE_NB_1: 273 | case C_SE_TB_1: 274 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 275 | default: 276 | return ErrTypeIDNotMatch 277 | } 278 | return c.Send(u) 279 | } 280 | 281 | // SetpointCommandFloatInfo 设定命令, 短浮点数 信息体 282 | type SetpointCommandFloatInfo struct { 283 | Ioa InfoObjAddr 284 | Value float32 285 | Qos QualifierOfSetpointCmd 286 | Time time.Time 287 | } 288 | 289 | // SetpointCmdFloat sends a type [C_SE_NC_1] or [C_SE_TC_1].设定命令,短浮点数,只有单个信息对象(SQ = 0) 290 | // [C_SE_NC_1] See companion standard 101, subclass 7.3.2.6 291 | // [C_SE_TC_1] See companion standard 101, 292 | // 传送原因(coa)用于 293 | // 控制方向: 294 | // <6> := 激活 295 | // <8> := 停止激活 296 | // 监视方向: 297 | // <7> := 激活确认 298 | // <9> := 停止激活确认 299 | // <10> := 激活终止 300 | // <44> := 未知的类型标识 301 | // <45> := 未知的传送原因 302 | // <46> := 未知的应用服务数据单元公共地址 303 | // <47> := 未知的信息对象地址 304 | func SetpointCmdFloat(c Connect, typeID TypeID, coa CauseOfTransmission, ca CommonAddr, cmd SetpointCommandFloatInfo) error { 305 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 306 | return ErrCmdCause 307 | } 308 | if err := c.Params().Valid(); err != nil { 309 | return err 310 | } 311 | u := NewASDU(c.Params(), Identifier{ 312 | typeID, 313 | VariableStruct{IsSequence: false, Number: 1}, 314 | coa, 315 | 0, 316 | ca, 317 | }) 318 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 319 | return err 320 | } 321 | 322 | u.AppendFloat32(cmd.Value).AppendBytes(cmd.Qos.Value()) 323 | 324 | switch typeID { 325 | case C_SE_NC_1: 326 | case C_SE_TC_1: 327 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 328 | default: 329 | return ErrTypeIDNotMatch 330 | } 331 | 332 | return c.Send(u) 333 | } 334 | 335 | // BitsString32CommandInfo 比特串命令 信息体 336 | type BitsString32CommandInfo struct { 337 | Ioa InfoObjAddr 338 | Value uint32 339 | Time time.Time 340 | } 341 | 342 | // BitsString32Cmd sends a type [C_BO_NA_1] or [C_BO_TA_1]. 比特串命令,只有单个信息对象(SQ = 0) 343 | // [C_BO_NA_1] See companion standard 101, subclass 7.3.2.7 344 | // [C_BO_TA_1] See companion standard 101, 345 | // 传送原因(coa)用于 346 | // 控制方向: 347 | // <6> := 激活 348 | // <8> := 停止激活 349 | // 监视方向: 350 | // <7> := 激活确认 351 | // <9> := 停止激活确认 352 | // <10> := 激活终止 353 | // <44> := 未知的类型标识 354 | // <45> := 未知的传送原因 355 | // <46> := 未知的应用服务数据单元公共地址 356 | // <47> := 未知的信息对象地址 357 | func BitsString32Cmd(c Connect, typeID TypeID, coa CauseOfTransmission, commonAddr CommonAddr, 358 | cmd BitsString32CommandInfo) error { 359 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 360 | return ErrCmdCause 361 | } 362 | if err := c.Params().Valid(); err != nil { 363 | return err 364 | } 365 | u := NewASDU(c.Params(), Identifier{ 366 | typeID, 367 | VariableStruct{IsSequence: false, Number: 1}, 368 | coa, 369 | 0, 370 | commonAddr, 371 | }) 372 | if err := u.AppendInfoObjAddr(cmd.Ioa); err != nil { 373 | return err 374 | } 375 | 376 | u.AppendBitsString32(cmd.Value) 377 | 378 | switch typeID { 379 | case C_BO_NA_1: 380 | case C_BO_TA_1: 381 | u.AppendBytes(CP56Time2a(cmd.Time, u.InfoObjTimeZone)...) 382 | default: 383 | return ErrTypeIDNotMatch 384 | } 385 | 386 | return c.Send(u) 387 | } 388 | 389 | // GetSingleCmd [C_SC_NA_1] or [C_SC_TA_1] 获取单命令信息体 390 | func (sf *ASDU) GetSingleCmd() SingleCommandInfo { 391 | var s SingleCommandInfo 392 | 393 | s.Ioa = sf.DecodeInfoObjAddr() 394 | value := sf.DecodeByte() 395 | s.Value = value&0x01 == 0x01 396 | s.Qoc = ParseQualifierOfCommand(value & 0xfe) 397 | 398 | switch sf.Type { 399 | case C_SC_NA_1: 400 | case C_SC_TA_1: 401 | s.Time = sf.DecodeCP56Time2a() 402 | default: 403 | panic(ErrTypeIDNotMatch) 404 | } 405 | 406 | return s 407 | } 408 | 409 | // GetDoubleCmd [C_DC_NA_1] or [C_DC_TA_1] 获取双命令信息体 410 | func (sf *ASDU) GetDoubleCmd() DoubleCommandInfo { 411 | var cmd DoubleCommandInfo 412 | 413 | cmd.Ioa = sf.DecodeInfoObjAddr() 414 | value := sf.DecodeByte() 415 | cmd.Value = DoubleCommand(value & 0x03) 416 | cmd.Qoc = ParseQualifierOfCommand(value & 0xfc) 417 | 418 | switch sf.Type { 419 | case C_DC_NA_1: 420 | case C_DC_TA_1: 421 | cmd.Time = sf.DecodeCP56Time2a() 422 | default: 423 | panic(ErrTypeIDNotMatch) 424 | } 425 | 426 | return cmd 427 | } 428 | 429 | // GetStepCmd [C_RC_NA_1] or [C_RC_TA_1] 获取步调节命令信息体 430 | func (sf *ASDU) GetStepCmd() StepCommandInfo { 431 | var cmd StepCommandInfo 432 | 433 | cmd.Ioa = sf.DecodeInfoObjAddr() 434 | value := sf.DecodeByte() 435 | cmd.Value = StepCommand(value & 0x03) 436 | cmd.Qoc = ParseQualifierOfCommand(value & 0xfc) 437 | 438 | switch sf.Type { 439 | case C_RC_NA_1: 440 | case C_RC_TA_1: 441 | cmd.Time = sf.DecodeCP56Time2a() 442 | default: 443 | panic(ErrTypeIDNotMatch) 444 | } 445 | 446 | return cmd 447 | } 448 | 449 | // GetSetpointNormalCmd [C_SE_NA_1] or [C_SE_TA_1] 获取设定命令,规一化值信息体 450 | func (sf *ASDU) GetSetpointNormalCmd() SetpointCommandNormalInfo { 451 | var cmd SetpointCommandNormalInfo 452 | 453 | cmd.Ioa = sf.DecodeInfoObjAddr() 454 | cmd.Value = sf.DecodeNormalize() 455 | cmd.Qos = ParseQualifierOfSetpointCmd(sf.DecodeByte()) 456 | 457 | switch sf.Type { 458 | case C_SE_NA_1: 459 | case C_SE_TA_1: 460 | cmd.Time = sf.DecodeCP56Time2a() 461 | default: 462 | panic(ErrTypeIDNotMatch) 463 | } 464 | 465 | return cmd 466 | } 467 | 468 | // GetSetpointCmdScaled [C_SE_NB_1] or [C_SE_TB_1] 获取设定命令,标度化值信息体 469 | func (sf *ASDU) GetSetpointCmdScaled() SetpointCommandScaledInfo { 470 | var cmd SetpointCommandScaledInfo 471 | 472 | cmd.Ioa = sf.DecodeInfoObjAddr() 473 | cmd.Value = sf.DecodeScaled() 474 | cmd.Qos = ParseQualifierOfSetpointCmd(sf.DecodeByte()) 475 | 476 | switch sf.Type { 477 | case C_SE_NB_1: 478 | case C_SE_TB_1: 479 | cmd.Time = sf.DecodeCP56Time2a() 480 | default: 481 | panic(ErrTypeIDNotMatch) 482 | } 483 | 484 | return cmd 485 | } 486 | 487 | // GetSetpointFloatCmd [C_SE_NC_1] or [C_SE_TC_1] 获取设定命令,短浮点数信息体 488 | func (sf *ASDU) GetSetpointFloatCmd() SetpointCommandFloatInfo { 489 | var cmd SetpointCommandFloatInfo 490 | 491 | cmd.Ioa = sf.DecodeInfoObjAddr() 492 | cmd.Value = sf.DecodeFloat32() 493 | cmd.Qos = ParseQualifierOfSetpointCmd(sf.DecodeByte()) 494 | 495 | switch sf.Type { 496 | case C_SE_NC_1: 497 | case C_SE_TC_1: 498 | cmd.Time = sf.DecodeCP56Time2a() 499 | default: 500 | panic(ErrTypeIDNotMatch) 501 | } 502 | 503 | return cmd 504 | } 505 | 506 | // GetBitsString32Cmd [C_BO_NA_1] or [C_BO_TA_1] 获取比特串命令信息体 507 | func (sf *ASDU) GetBitsString32Cmd() BitsString32CommandInfo { 508 | var cmd BitsString32CommandInfo 509 | 510 | cmd.Ioa = sf.DecodeInfoObjAddr() 511 | cmd.Value = sf.DecodeBitsString32() 512 | switch sf.Type { 513 | case C_BO_NA_1: 514 | case C_BO_TA_1: 515 | cmd.Time = sf.DecodeCP56Time2a() 516 | default: 517 | panic(ErrTypeIDNotMatch) 518 | } 519 | 520 | return cmd 521 | } 522 | -------------------------------------------------------------------------------- /asdu/csys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "time" 9 | ) 10 | 11 | // 在控制方向系统信息的应用服务数据单元 12 | 13 | // InterrogationCmd send a new interrogation command [C_IC_NA_1]. 总召唤命令, 只有单个信息对象(SQ = 0) 14 | // [C_IC_NA_1] See companion standard 101, subclass 7.3.4.1 15 | // 传送原因(coa)用于 16 | // 控制方向: 17 | // <6> := 激活 18 | // <8> := 停止激活 19 | // 监视方向: 20 | // <7> := 激活确认 21 | // <9> := 停止激活确认 22 | // <10> := 激活终止 23 | // <44> := 未知的类型标识 24 | // <45> := 未知的传送原因 25 | // <46> := 未知的应用服务数据单元公共地址 26 | // <47> := 未知的信息对象地址 27 | func InterrogationCmd(c Connect, coa CauseOfTransmission, ca CommonAddr, qoi QualifierOfInterrogation) error { 28 | if !(coa.Cause == Activation || coa.Cause == Deactivation) { 29 | return ErrCmdCause 30 | } 31 | if err := c.Params().Valid(); err != nil { 32 | return err 33 | } 34 | 35 | u := NewASDU(c.Params(), Identifier{ 36 | C_IC_NA_1, 37 | VariableStruct{IsSequence: false, Number: 1}, 38 | coa, 39 | 0, 40 | ca, 41 | }) 42 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 43 | return err 44 | } 45 | u.AppendBytes(byte(qoi)) 46 | return c.Send(u) 47 | } 48 | 49 | // CounterInterrogationCmd send Counter Interrogation command [C_CI_NA_1],计数量召唤命令,只有单个信息对象(SQ = 0) 50 | // [C_CI_NA_1] See companion standard 101, subclass 7.3.4.2 51 | // 传送原因(coa)用于 52 | // 控制方向: 53 | // <6> := 激活 54 | // 监视方向: 55 | // <7> := 激活确认 56 | // <10> := 激活终止 57 | // <44> := 未知的类型标识 58 | // <45> := 未知的传送原因 59 | // <46> := 未知的应用服务数据单元公共地址 60 | // <47> := 未知的信息对象地址 61 | func CounterInterrogationCmd(c Connect, coa CauseOfTransmission, ca CommonAddr, qcc QualifierCountCall) error { 62 | if err := c.Params().Valid(); err != nil { 63 | return err 64 | } 65 | coa.Cause = Activation 66 | u := NewASDU(c.Params(), Identifier{ 67 | C_CI_NA_1, 68 | VariableStruct{IsSequence: false, Number: 1}, 69 | coa, 70 | 0, 71 | ca, 72 | }) 73 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 74 | return err 75 | } 76 | u.AppendBytes(qcc.Value()) 77 | return c.Send(u) 78 | } 79 | 80 | // ReadCmd send read command [C_RD_NA_1], 读命令, 只有单个信息对象(SQ = 0) 81 | // [C_RD_NA_1] See companion standard 101, subclass 7.3.4.3 82 | // 传送原因(coa)用于 83 | // 控制方向: 84 | // <5> := 请求 85 | // 监视方向: 86 | // <44> := 未知的类型标识 87 | // <45> := 未知的传送原因 88 | // <46> := 未知的应用服务数据单元公共地址 89 | // <47> := 未知的信息对象地址 90 | func ReadCmd(c Connect, coa CauseOfTransmission, ca CommonAddr, ioa InfoObjAddr) error { 91 | if err := c.Params().Valid(); err != nil { 92 | return err 93 | } 94 | coa.Cause = Request 95 | u := NewASDU(c.Params(), Identifier{ 96 | C_RD_NA_1, 97 | VariableStruct{IsSequence: false, Number: 1}, 98 | coa, 99 | 0, 100 | ca, 101 | }) 102 | if err := u.AppendInfoObjAddr(ioa); err != nil { 103 | return err 104 | } 105 | return c.Send(u) 106 | } 107 | 108 | // ClockSynchronizationCmd send clock sync command [C_CS_NA_1],时钟同步命令, 只有单个信息对象(SQ = 0) 109 | // [C_CS_NA_1] See companion standard 101, subclass 7.3.4.4 110 | // 传送原因(coa)用于 111 | // 控制方向: 112 | // <6> := 激活 113 | // 监视方向: 114 | // <7> := 激活确认 115 | // <10> := 激活终止 116 | // <44> := 未知的类型标识 117 | // <45> := 未知的传送原因 118 | // <46> := 未知的应用服务数据单元公共地址 119 | // <47> := 未知的信息对象地址 120 | func ClockSynchronizationCmd(c Connect, coa CauseOfTransmission, ca CommonAddr, t time.Time) error { 121 | if err := c.Params().Valid(); err != nil { 122 | return err 123 | } 124 | coa.Cause = Activation 125 | u := NewASDU(c.Params(), Identifier{ 126 | C_CS_NA_1, 127 | VariableStruct{IsSequence: false, Number: 1}, 128 | coa, 129 | 0, 130 | ca, 131 | }) 132 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 133 | return err 134 | } 135 | u.AppendBytes(CP56Time2a(t, u.InfoObjTimeZone)...) 136 | return c.Send(u) 137 | } 138 | 139 | // TestCommand send test command [C_TS_NA_1],测试命令, 只有单个信息对象(SQ = 0) 140 | // [C_TS_NA_1] See companion standard 101, subclass 7.3.4.5 141 | // 传送原因(coa)用于 142 | // 控制方向: 143 | // <6> := 激活 144 | // 监视方向: 145 | // <7> := 激活确认 146 | // <44> := 未知的类型标识 147 | // <45> := 未知的传送原因 148 | // <46> := 未知的应用服务数据单元公共地址 149 | // <47> := 未知的信息对象地址 150 | func TestCommand(c Connect, coa CauseOfTransmission, ca CommonAddr) error { 151 | if err := c.Params().Valid(); err != nil { 152 | return err 153 | } 154 | coa.Cause = Activation 155 | u := NewASDU(c.Params(), Identifier{ 156 | C_TS_NA_1, 157 | VariableStruct{IsSequence: false, Number: 1}, 158 | coa, 159 | 0, 160 | ca, 161 | }) 162 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 163 | return err 164 | } 165 | u.AppendBytes(byte(FBPTestWord&0xff), byte(FBPTestWord>>8)) 166 | return c.Send(u) 167 | } 168 | 169 | // ResetProcessCmd send reset process command [C_RP_NA_1],复位进程命令, 只有单个信息对象(SQ = 0) 170 | // [C_RP_NA_1] See companion standard 101, subclass 7.3.4.6 171 | // 传送原因(coa)用于 172 | // 控制方向: 173 | // <6> := 激活 174 | // 监视方向: 175 | // <7> := 激活确认 176 | // <44> := 未知的类型标识 177 | // <45> := 未知的传送原因 178 | // <46> := 未知的应用服务数据单元公共地址 179 | // <47> := 未知的信息对象地址 180 | func ResetProcessCmd(c Connect, coa CauseOfTransmission, ca CommonAddr, qrp QualifierOfResetProcessCmd) error { 181 | if err := c.Params().Valid(); err != nil { 182 | return err 183 | } 184 | coa.Cause = Activation 185 | u := NewASDU(c.Params(), Identifier{ 186 | C_RP_NA_1, 187 | VariableStruct{IsSequence: false, Number: 1}, 188 | coa, 189 | 0, 190 | ca, 191 | }) 192 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 193 | return err 194 | } 195 | u.AppendBytes(byte(qrp)) 196 | return c.Send(u) 197 | } 198 | 199 | // DelayAcquireCommand send delay acquire command [C_CD_NA_1],延时获得命令, 只有单个信息对象(SQ = 0) 200 | // [C_CD_NA_1] See companion standard 101, subclass 7.3.4.7 201 | // 传送原因(coa)用于 202 | // 控制方向: 203 | // <3> := 突发 204 | // <6> := 激活 205 | // 监视方向: 206 | // <7> := 激活确认 207 | // <44> := 未知的类型标识 208 | // <45> := 未知的传送原因 209 | // <46> := 未知的应用服务数据单元公共地址 210 | // <47> := 未知的信息对象地址 211 | func DelayAcquireCommand(c Connect, coa CauseOfTransmission, ca CommonAddr, msec uint16) error { 212 | if !(coa.Cause == Spontaneous || coa.Cause == Activation) { 213 | return ErrCmdCause 214 | } 215 | if err := c.Params().Valid(); err != nil { 216 | return err 217 | } 218 | 219 | u := NewASDU(c.Params(), Identifier{ 220 | C_CD_NA_1, 221 | VariableStruct{IsSequence: false, Number: 1}, 222 | coa, 223 | 0, 224 | ca, 225 | }) 226 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 227 | return err 228 | } 229 | u.AppendCP16Time2a(msec) 230 | return c.Send(u) 231 | } 232 | 233 | // TestCommandCP56Time2a send test command [C_TS_TA_1],测试命令, 只有单个信息对象(SQ = 0) 234 | // 传送原因(coa)用于 235 | // 控制方向: 236 | // <6> := 激活 237 | // 监视方向: 238 | // <7> := 激活确认 239 | // <44> := 未知的类型标识 240 | // <45> := 未知的传送原因 241 | // <46> := 未知的应用服务数据单元公共地址 242 | // <47> := 未知的信息对象地址 243 | func TestCommandCP56Time2a(c Connect, coa CauseOfTransmission, ca CommonAddr, t time.Time) error { 244 | if err := c.Params().Valid(); err != nil { 245 | return err 246 | } 247 | u := NewASDU(c.Params(), Identifier{ 248 | C_TS_TA_1, 249 | VariableStruct{IsSequence: false, Number: 1}, 250 | coa, 251 | 0, 252 | ca, 253 | }) 254 | if err := u.AppendInfoObjAddr(InfoObjAddrIrrelevant); err != nil { 255 | return err 256 | } 257 | u.AppendUint16(FBPTestWord) 258 | u.AppendCP56Time2a(t, u.InfoObjTimeZone) 259 | return c.Send(u) 260 | } 261 | 262 | // GetInterrogationCmd [C_IC_NA_1] 获取总召唤信息体(信息对象地址,召唤限定词) 263 | func (sf *ASDU) GetInterrogationCmd() (InfoObjAddr, QualifierOfInterrogation) { 264 | return sf.DecodeInfoObjAddr(), QualifierOfInterrogation(sf.infoObj[0]) 265 | } 266 | 267 | // GetCounterInterrogationCmd [C_CI_NA_1] 获得计量召唤信息体(信息对象地址,计量召唤限定词) 268 | func (sf *ASDU) GetCounterInterrogationCmd() (InfoObjAddr, QualifierCountCall) { 269 | return sf.DecodeInfoObjAddr(), ParseQualifierCountCall(sf.infoObj[0]) 270 | } 271 | 272 | // GetReadCmd [C_RD_NA_1] 获得读命令信息地址 273 | func (sf *ASDU) GetReadCmd() InfoObjAddr { 274 | return sf.DecodeInfoObjAddr() 275 | } 276 | 277 | // GetClockSynchronizationCmd [C_CS_NA_1] 获得时钟同步命令信息体(信息对象地址,时间) 278 | func (sf *ASDU) GetClockSynchronizationCmd() (InfoObjAddr, time.Time) { 279 | 280 | return sf.DecodeInfoObjAddr(), sf.DecodeCP56Time2a() 281 | } 282 | 283 | // GetTestCommand [C_TS_NA_1],获得测试命令信息体(信息对象地址,是否是测试字) 284 | func (sf *ASDU) GetTestCommand() (InfoObjAddr, bool) { 285 | return sf.DecodeInfoObjAddr(), sf.DecodeUint16() == FBPTestWord 286 | } 287 | 288 | // GetResetProcessCmd [C_RP_NA_1] 获得复位进程命令信息体(信息对象地址,复位进程命令限定词) 289 | func (sf *ASDU) GetResetProcessCmd() (InfoObjAddr, QualifierOfResetProcessCmd) { 290 | return sf.DecodeInfoObjAddr(), QualifierOfResetProcessCmd(sf.infoObj[0]) 291 | } 292 | 293 | // GetDelayAcquireCommand [C_CD_NA_1] 获取延时获取命令信息体(信息对象地址,延时毫秒数) 294 | func (sf *ASDU) GetDelayAcquireCommand() (InfoObjAddr, uint16) { 295 | return sf.DecodeInfoObjAddr(), sf.DecodeUint16() 296 | } 297 | 298 | // GetTestCommandCP56Time2a [C_TS_TA_1],获得测试命令信息体(信息对象地址,是否是测试字) 299 | func (sf *ASDU) GetTestCommandCP56Time2a() (InfoObjAddr, bool, time.Time) { 300 | return sf.DecodeInfoObjAddr(), sf.DecodeUint16() == FBPTestWord, sf.DecodeCP56Time2a() 301 | } 302 | -------------------------------------------------------------------------------- /asdu/csys_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestInterrogationCmd(t *testing.T) { 10 | type args struct { 11 | c Connect 12 | coa CauseOfTransmission 13 | ca CommonAddr 14 | qoi QualifierOfInterrogation 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantErr bool 20 | }{ 21 | { 22 | "cause not Activation and Deactivation", 23 | args{ 24 | newConn(nil, t), 25 | CauseOfTransmission{Cause: Unused}, 26 | 0x1234, 27 | QOIGroup1}, 28 | true, 29 | }, 30 | { 31 | "C_IC_NA_1", 32 | args{ 33 | newConn([]byte{byte(C_IC_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 34 | 0x00, 0x00, 0x00, 21}, t), 35 | CauseOfTransmission{Cause: Activation}, 36 | 0x1234, 37 | QOIGroup1}, 38 | false, 39 | }, 40 | } 41 | for _, tt := range tests { 42 | t.Run(tt.name, func(t *testing.T) { 43 | if err := InterrogationCmd(tt.args.c, tt.args.coa, tt.args.ca, tt.args.qoi); (err != nil) != tt.wantErr { 44 | t.Errorf("InterrogationCmd() error = %v, wantErr %v", err, tt.wantErr) 45 | } 46 | }) 47 | } 48 | } 49 | 50 | func TestCounterInterrogationCmd(t *testing.T) { 51 | type args struct { 52 | c Connect 53 | coa CauseOfTransmission 54 | ca CommonAddr 55 | qcc QualifierCountCall 56 | } 57 | tests := []struct { 58 | name string 59 | args args 60 | wantErr bool 61 | }{ 62 | { 63 | "C_CI_NA_1", 64 | args{ 65 | newConn([]byte{byte(C_CI_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 66 | 0x00, 0x00, 0x00, 0x01}, t), 67 | CauseOfTransmission{Cause: Activation}, 68 | 0x1234, 69 | QualifierCountCall{QCCGroup1, QCCFrzRead}}, 70 | false, 71 | }, 72 | } 73 | for _, tt := range tests { 74 | t.Run(tt.name, func(t *testing.T) { 75 | if err := CounterInterrogationCmd(tt.args.c, tt.args.coa, tt.args.ca, tt.args.qcc); (err != nil) != tt.wantErr { 76 | t.Errorf("CounterInterrogationCmd() error = %v, wantErr %v", err, tt.wantErr) 77 | } 78 | }) 79 | } 80 | } 81 | 82 | func TestReadCmd(t *testing.T) { 83 | type args struct { 84 | c Connect 85 | coa CauseOfTransmission 86 | ca CommonAddr 87 | ioa InfoObjAddr 88 | } 89 | tests := []struct { 90 | name string 91 | args args 92 | wantErr bool 93 | }{ 94 | { 95 | "C_RD_NA_1", 96 | args{ 97 | newConn([]byte{byte(C_RD_NA_1), 0x01, 0x05, 0x00, 0x34, 0x12, 98 | 0x90, 0x78, 0x56}, t), 99 | CauseOfTransmission{Cause: Request}, 100 | 0x1234, 101 | 0x567890}, 102 | false, 103 | }, 104 | } 105 | for _, tt := range tests { 106 | t.Run(tt.name, func(t *testing.T) { 107 | if err := ReadCmd(tt.args.c, tt.args.coa, tt.args.ca, tt.args.ioa); (err != nil) != tt.wantErr { 108 | t.Errorf("ReadCmd() error = %v, wantErr %v", err, tt.wantErr) 109 | } 110 | }) 111 | } 112 | } 113 | 114 | func TestClockSynchronizationCmd(t *testing.T) { 115 | type args struct { 116 | c Connect 117 | coa CauseOfTransmission 118 | ca CommonAddr 119 | t time.Time 120 | } 121 | tests := []struct { 122 | name string 123 | args args 124 | wantErr bool 125 | }{ 126 | { 127 | "C_CS_NA_1", 128 | args{ 129 | newConn(append([]byte{byte(C_CS_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 130 | 0x00, 0x00, 0x00}, tm0CP56Time2aBytes...), t), 131 | CauseOfTransmission{Cause: Activation}, 132 | 0x1234, 133 | tm0}, 134 | false, 135 | }, 136 | } 137 | for _, tt := range tests { 138 | t.Run(tt.name, func(t *testing.T) { 139 | if err := ClockSynchronizationCmd(tt.args.c, tt.args.coa, tt.args.ca, tt.args.t); (err != nil) != tt.wantErr { 140 | t.Errorf("ClockSynchronizationCmd() error = %v, wantErr %v", err, tt.wantErr) 141 | } 142 | }) 143 | } 144 | } 145 | 146 | func TestTestCommand(t *testing.T) { 147 | type args struct { 148 | c Connect 149 | coa CauseOfTransmission 150 | ca CommonAddr 151 | } 152 | tests := []struct { 153 | name string 154 | args args 155 | wantErr bool 156 | }{ 157 | { 158 | "C_TS_NA_1", 159 | args{ 160 | newConn([]byte{byte(C_TS_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 161 | 0x00, 0x00, 0x00, 0xaa, 0x55}, t), 162 | CauseOfTransmission{Cause: Activation}, 163 | 0x1234}, 164 | false, 165 | }, 166 | } 167 | for _, tt := range tests { 168 | t.Run(tt.name, func(t *testing.T) { 169 | if err := TestCommand(tt.args.c, tt.args.coa, tt.args.ca); (err != nil) != tt.wantErr { 170 | t.Errorf("TestCommand() error = %v, wantErr %v", err, tt.wantErr) 171 | } 172 | }) 173 | } 174 | } 175 | 176 | func TestResetProcessCmd(t *testing.T) { 177 | type args struct { 178 | c Connect 179 | coa CauseOfTransmission 180 | ca CommonAddr 181 | qrp QualifierOfResetProcessCmd 182 | } 183 | tests := []struct { 184 | name string 185 | args args 186 | wantErr bool 187 | }{ 188 | { 189 | "C_RP_NA_1", 190 | args{ 191 | newConn([]byte{byte(C_RP_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 192 | 0x00, 0x00, 0x00, 0x01}, t), 193 | CauseOfTransmission{Cause: Activation}, 194 | 0x1234, 195 | QPRGeneralRest}, 196 | false, 197 | }, 198 | } 199 | for _, tt := range tests { 200 | t.Run(tt.name, func(t *testing.T) { 201 | if err := ResetProcessCmd(tt.args.c, tt.args.coa, tt.args.ca, tt.args.qrp); (err != nil) != tt.wantErr { 202 | t.Errorf("ResetProcessCmd() error = %v, wantErr %v", err, tt.wantErr) 203 | } 204 | }) 205 | } 206 | } 207 | 208 | func TestDelayAcquireCommand(t *testing.T) { 209 | type args struct { 210 | c Connect 211 | coa CauseOfTransmission 212 | ca CommonAddr 213 | msec uint16 214 | } 215 | tests := []struct { 216 | name string 217 | args args 218 | wantErr bool 219 | }{ 220 | { 221 | "cause not act and spont", 222 | args{ 223 | newConn(nil, t), 224 | CauseOfTransmission{Cause: Unused}, 225 | 0x1234, 226 | 10000}, 227 | true, 228 | }, 229 | { 230 | "C_CD_NA_1", 231 | args{ 232 | newConn([]byte{byte(C_CD_NA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 233 | 0x00, 0x00, 0x00, 0x10, 0x27}, t), 234 | CauseOfTransmission{Cause: Activation}, 235 | 0x1234, 236 | 10000}, 237 | false, 238 | }, 239 | } 240 | for _, tt := range tests { 241 | t.Run(tt.name, func(t *testing.T) { 242 | if err := DelayAcquireCommand(tt.args.c, tt.args.coa, tt.args.ca, tt.args.msec); (err != nil) != tt.wantErr { 243 | t.Errorf("DelayAcquireCommand() error = %v, wantErr %v", err, tt.wantErr) 244 | } 245 | }) 246 | } 247 | } 248 | 249 | func TestTestCommandCP56Time2a(t *testing.T) { 250 | type args struct { 251 | c Connect 252 | coa CauseOfTransmission 253 | ca CommonAddr 254 | t time.Time 255 | } 256 | tests := []struct { 257 | name string 258 | args args 259 | wantErr bool 260 | }{ 261 | { 262 | "C_TS_TA_1", 263 | args{ 264 | newConn(append([]byte{byte(C_TS_TA_1), 0x01, 0x06, 0x00, 0x34, 0x12, 265 | 0x00, 0x00, 0x00, 0xaa, 0x55}, tm0CP56Time2aBytes...), t), 266 | CauseOfTransmission{Cause: Activation}, 267 | 0x1234, 268 | tm0}, 269 | false, 270 | }, 271 | } 272 | for _, tt := range tests { 273 | t.Run(tt.name, func(t *testing.T) { 274 | if err := TestCommandCP56Time2a(tt.args.c, tt.args.coa, tt.args.ca, tt.args.t); (err != nil) != tt.wantErr { 275 | t.Errorf("TestCommandCP56Time2a() error = %v, wantErr %v", err, tt.wantErr) 276 | } 277 | }) 278 | } 279 | } 280 | 281 | func TestASDU_GetInterrogationCmd(t *testing.T) { 282 | type fields struct { 283 | Params *Params 284 | infoObj []byte 285 | } 286 | tests := []struct { 287 | name string 288 | fields fields 289 | want InfoObjAddr 290 | want1 QualifierOfInterrogation 291 | }{ 292 | { 293 | "C_IC_NA_1", 294 | fields{ParamsWide, []byte{0x00, 0x00, 0x00, 21}}, 295 | 0, 296 | QOIGroup1, 297 | }, 298 | } 299 | for _, tt := range tests { 300 | t.Run(tt.name, func(t *testing.T) { 301 | this := &ASDU{ 302 | Params: tt.fields.Params, 303 | infoObj: tt.fields.infoObj, 304 | } 305 | got, got1 := this.GetInterrogationCmd() 306 | if got != tt.want { 307 | t.Errorf("ASDU.GetInterrogationCmd() QOI = %v, want %v", got, tt.want) 308 | } 309 | if got1 != tt.want1 { 310 | t.Errorf("ASDU.GetInterrogationCmd() InfoObjAddr = %v, want %v", got1, tt.want1) 311 | } 312 | 313 | }) 314 | } 315 | } 316 | 317 | func TestASDU_GetCounterInterrogationCmd(t *testing.T) { 318 | type fields struct { 319 | Params *Params 320 | infoObj []byte 321 | } 322 | tests := []struct { 323 | name string 324 | fields fields 325 | want InfoObjAddr 326 | want1 QualifierCountCall 327 | }{ 328 | { 329 | "C_CI_NA_1", 330 | fields{ParamsWide, []byte{0x00, 0x00, 0x00, 0x01}}, 331 | 0, 332 | QualifierCountCall{QCCGroup1, QCCFrzRead}, 333 | }, 334 | } 335 | for _, tt := range tests { 336 | t.Run(tt.name, func(t *testing.T) { 337 | this := &ASDU{ 338 | Params: tt.fields.Params, 339 | infoObj: tt.fields.infoObj, 340 | } 341 | got, got1 := this.GetCounterInterrogationCmd() 342 | if got != tt.want { 343 | t.Errorf("ASDU.GetQuantityInterrogationCmd() InfoObjAddr = %v, want %v", got, tt.want) 344 | } 345 | if !reflect.DeepEqual(got1, tt.want1) { 346 | t.Errorf("ASDU.GetQuantityInterrogationCmd() QCC = %v, want %v", got1, tt.want1) 347 | } 348 | }) 349 | } 350 | } 351 | 352 | func TestASDU_GetReadCmd(t *testing.T) { 353 | type fields struct { 354 | Params *Params 355 | infoObj []byte 356 | } 357 | tests := []struct { 358 | name string 359 | fields fields 360 | want InfoObjAddr 361 | }{ 362 | { 363 | "C_RD_NA_1", 364 | fields{ParamsWide, []byte{0x90, 0x78, 0x56}}, 365 | 0x567890, 366 | }, 367 | } 368 | for _, tt := range tests { 369 | t.Run(tt.name, func(t *testing.T) { 370 | this := &ASDU{ 371 | Params: tt.fields.Params, 372 | infoObj: tt.fields.infoObj, 373 | } 374 | got := this.GetReadCmd() 375 | if got != tt.want { 376 | t.Errorf("ASDU.GetReadCmd() = %v, want %v", got, tt.want) 377 | } 378 | }) 379 | } 380 | } 381 | 382 | func TestASDU_GetClockSynchronizationCmd(t *testing.T) { 383 | type fields struct { 384 | Params *Params 385 | infoObj []byte 386 | } 387 | tests := []struct { 388 | name string 389 | fields fields 390 | want InfoObjAddr 391 | want1 time.Time 392 | }{ 393 | { 394 | "C_CS_NA_1", 395 | fields{ParamsWide, append([]byte{0x00, 0x00, 0x00}, tm0CP56Time2aBytes...)}, 396 | 0, 397 | tm0, 398 | }, 399 | } 400 | for _, tt := range tests { 401 | t.Run(tt.name, func(t *testing.T) { 402 | this := &ASDU{ 403 | Params: tt.fields.Params, 404 | infoObj: tt.fields.infoObj, 405 | } 406 | got, got1 := this.GetClockSynchronizationCmd() 407 | if got != tt.want { 408 | t.Errorf("ASDU.GetClockSynchronizationCmd() InfoObjAddr = %v, want %v", got, tt.want) 409 | } 410 | if !reflect.DeepEqual(got1, tt.want1) { 411 | t.Errorf("ASDU.GetClockSynchronizationCmd() time = %v, want %v", got1, tt.want1) 412 | } 413 | }) 414 | } 415 | } 416 | 417 | func TestASDU_GetTestCommand(t *testing.T) { 418 | type fields struct { 419 | Params *Params 420 | infoObj []byte 421 | } 422 | tests := []struct { 423 | name string 424 | fields fields 425 | want InfoObjAddr 426 | want1 bool 427 | }{ 428 | { 429 | "C_CS_NA_1", 430 | fields{ParamsWide, []byte{0x00, 0x00, 0x00, 0xaa, 0x55}}, 431 | 0, 432 | true, 433 | }, 434 | } 435 | for _, tt := range tests { 436 | t.Run(tt.name, func(t *testing.T) { 437 | this := &ASDU{ 438 | Params: tt.fields.Params, 439 | infoObj: tt.fields.infoObj, 440 | } 441 | got, got1 := this.GetTestCommand() 442 | if got != tt.want { 443 | t.Errorf("ASDU.GetTestCommand() InfoObjAddr = %v, want %v", got, tt.want) 444 | } 445 | if got1 != tt.want1 { 446 | t.Errorf("ASDU.GetTestCommand() bool = %v, want %v", got1, tt.want1) 447 | } 448 | }) 449 | } 450 | } 451 | 452 | func TestASDU_GetResetProcessCmd(t *testing.T) { 453 | type fields struct { 454 | Params *Params 455 | infoObj []byte 456 | } 457 | tests := []struct { 458 | name string 459 | fields fields 460 | want InfoObjAddr 461 | want1 QualifierOfResetProcessCmd 462 | }{ 463 | { 464 | "C_RP_NA_1", 465 | fields{ParamsWide, []byte{0x00, 0x00, 0x00, 0x01}}, 466 | 0, 467 | QPRGeneralRest, 468 | }, 469 | } 470 | for _, tt := range tests { 471 | t.Run(tt.name, func(t *testing.T) { 472 | this := &ASDU{ 473 | Params: tt.fields.Params, 474 | infoObj: tt.fields.infoObj, 475 | } 476 | got, got1 := this.GetResetProcessCmd() 477 | if got != tt.want { 478 | t.Errorf("ASDU.GetResetProcessCmd() InfoObjAddr = %v, want %v", got, tt.want) 479 | } 480 | if got1 != tt.want1 { 481 | t.Errorf("ASDU.GetResetProcessCmd() QOP = %v, want %v", got1, tt.want1) 482 | } 483 | }) 484 | } 485 | } 486 | 487 | func TestASDU_GetDelayAcquireCommand(t *testing.T) { 488 | type fields struct { 489 | Params *Params 490 | infoObj []byte 491 | } 492 | tests := []struct { 493 | name string 494 | fields fields 495 | want InfoObjAddr 496 | want1 uint16 497 | }{ 498 | { 499 | "C_CD_NA_1", 500 | fields{ParamsWide, []byte{0x00, 0x00, 0x00, 0x10, 0x27}}, 501 | 0, 502 | 10000, 503 | }, 504 | } 505 | for _, tt := range tests { 506 | t.Run(tt.name, func(t *testing.T) { 507 | this := &ASDU{ 508 | Params: tt.fields.Params, 509 | infoObj: tt.fields.infoObj, 510 | } 511 | got, got1 := this.GetDelayAcquireCommand() 512 | if got != tt.want { 513 | t.Errorf("ASDU.GetDelayAcquireCommand() InfoObjAddr = %v, want %v", got, tt.want) 514 | } 515 | if got1 != tt.want1 { 516 | t.Errorf("ASDU.GetDelayAcquireCommand() msec = %v, want %v", got1, tt.want1) 517 | } 518 | }) 519 | } 520 | } 521 | 522 | func TestASDU_GetTestCommandCP56Time2a(t *testing.T) { 523 | type fields struct { 524 | Params *Params 525 | infoObj []byte 526 | } 527 | tests := []struct { 528 | name string 529 | fields fields 530 | want InfoObjAddr 531 | want1 bool 532 | want2 time.Time 533 | }{ 534 | { 535 | "C_CS_TA_1", 536 | fields{ParamsWide, append([]byte{0x00, 0x00, 0x00, 0xaa, 0x55}, tm0CP56Time2aBytes...)}, 537 | 0, 538 | true, 539 | tm0, 540 | }, 541 | } 542 | for _, tt := range tests { 543 | t.Run(tt.name, func(t *testing.T) { 544 | sf := &ASDU{ 545 | Params: tt.fields.Params, 546 | infoObj: tt.fields.infoObj, 547 | } 548 | got, got1, got2 := sf.GetTestCommandCP56Time2a() 549 | if got != tt.want { 550 | t.Errorf("GetTestCommandCP56Time2a() got = %v, want %v", got, tt.want) 551 | } 552 | if !reflect.DeepEqual(got1, tt.want1) { 553 | t.Errorf("GetTestCommandCP56Time2a() got1 = %v, want %v", got1, tt.want1) 554 | } 555 | if got2 != tt.want2 { 556 | t.Errorf("GetTestCommandCP56Time2a() got2 = %v, want %v", got2, tt.want2) 557 | } 558 | }) 559 | } 560 | } 561 | -------------------------------------------------------------------------------- /asdu/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // error defined 13 | var ( 14 | ErrTypeIdentifier = errors.New("asdu: type identification unknown") 15 | ErrCauseZero = errors.New("asdu: cause of transmission 0 is not used") 16 | ErrCommonAddrZero = errors.New("asdu: common address 0 is not used") 17 | 18 | ErrParam = errors.New("asdu: system parameter out of range") 19 | ErrInvalidTimeTag = errors.New("asdu: invalid time tag") 20 | ErrOriginAddrFit = errors.New("asdu: originator address not allowed with cause size 1 system parameter") 21 | ErrCommonAddrFit = errors.New("asdu: common address exceeds size system parameter") 22 | ErrInfoObjAddrFit = errors.New("asdu: information object address exceeds size system parameter") 23 | ErrInfoObjIndexFit = errors.New("asdu: information object index not in [1, 127]") 24 | ErrInroGroupNumFit = errors.New("asdu: interrogation group number exceeds 16") 25 | 26 | ErrLengthOutOfRange = fmt.Errorf("asdu: asdu filed length large than max %d", ASDUSizeMax) 27 | ErrNotAnyObjInfo = errors.New("asdu: not any object information") 28 | ErrTypeIDNotMatch = errors.New("asdu: type identifier doesn't match call or time tag") 29 | 30 | ErrCmdCause = errors.New("asdu: cause of transmission for command not standard requirement") 31 | ) 32 | -------------------------------------------------------------------------------- /asdu/filet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | // 文件传输的应用服务数据单元 8 | // TODO: 9 | -------------------------------------------------------------------------------- /asdu/identifier_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestGetInfoObjSize(t *testing.T) { 9 | type args struct { 10 | id TypeID 11 | } 12 | tests := []struct { 13 | name string 14 | args args 15 | want int 16 | wantErr bool 17 | }{ 18 | {"defined", args{F_DR_TA_1}, 13, false}, 19 | {"no defined", args{F_SG_NA_1}, 0, true}, 20 | } 21 | for _, tt := range tests { 22 | t.Run(tt.name, func(t *testing.T) { 23 | got, err := GetInfoObjSize(tt.args.id) 24 | if (err != nil) != tt.wantErr { 25 | t.Errorf("GetInfoObjSize() error = %v, wantErr %v", err, tt.wantErr) 26 | return 27 | } 28 | if got != tt.want { 29 | t.Errorf("GetInfoObjSize() = %v, want %v", got, tt.want) 30 | } 31 | }) 32 | } 33 | } 34 | 35 | func TestTypeID_String(t *testing.T) { 36 | tests := []struct { 37 | name string 38 | this TypeID 39 | want string 40 | }{ 41 | {"M_SP_NA_1", M_SP_NA_1, "TID"}, 42 | {"M_SP_TB_1", M_SP_TB_1, "TID"}, 43 | {"C_SC_NA_1", C_SC_NA_1, "TID"}, 44 | {"C_SC_TA_1", C_SC_TA_1, "TID"}, 45 | {"M_EI_NA_1", M_EI_NA_1, "TID"}, 46 | {"S_CH_NA_1", S_CH_NA_1, "TID"}, 47 | {"S_US_NA_1", S_US_NA_1, "TID"}, 48 | {"C_IC_NA_1", C_IC_NA_1, "TID"}, 49 | {"P_ME_NA_1", P_ME_NA_1, "TID"}, 50 | {"F_FR_NA_1", F_FR_NA_1, "TID"}, 51 | {"no defined", 0, "TID<0>"}, 52 | } 53 | for _, tt := range tests { 54 | t.Run(tt.name, func(t *testing.T) { 55 | if got := tt.this.String(); got != tt.want { 56 | t.Errorf("TypeID.String() = %v, want %v", got, tt.want) 57 | } 58 | }) 59 | } 60 | } 61 | 62 | func TestParseVariableStruct(t *testing.T) { 63 | type args struct { 64 | b byte 65 | } 66 | tests := []struct { 67 | name string 68 | args args 69 | want VariableStruct 70 | }{ 71 | {"no sequence", args{0x0a}, VariableStruct{Number: 0x0a}}, 72 | {"with sequence", args{0x8a}, VariableStruct{Number: 0x0a, IsSequence: true}}, 73 | } 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | if got := ParseVariableStruct(tt.args.b); !reflect.DeepEqual(got, tt.want) { 77 | t.Errorf("ParseVariableStruct() = %v, want %v", got, tt.want) 78 | } 79 | }) 80 | } 81 | } 82 | 83 | func TestVariableStruct_Value(t *testing.T) { 84 | tests := []struct { 85 | name string 86 | this VariableStruct 87 | want byte 88 | }{ 89 | {"no sequence", VariableStruct{Number: 0x0a}, 0x0a}, 90 | {"with sequence", VariableStruct{Number: 0x0a, IsSequence: true}, 0x8a}, 91 | } 92 | for _, tt := range tests { 93 | t.Run(tt.name, func(t *testing.T) { 94 | if got := tt.this.Value(); got != tt.want { 95 | t.Errorf("VariableStruct.Value() = %v, want %v", got, tt.want) 96 | } 97 | }) 98 | } 99 | } 100 | 101 | func TestVariableStruct_String(t *testing.T) { 102 | tests := []struct { 103 | name string 104 | this VariableStruct 105 | want string 106 | }{ 107 | {"no sequence", VariableStruct{Number: 100}, "VSQ<100>"}, 108 | {"with sequence", VariableStruct{Number: 100, IsSequence: true}, "VSQ"}, 109 | } 110 | for _, tt := range tests { 111 | t.Run(tt.name, func(t *testing.T) { 112 | if got := tt.this.String(); got != tt.want { 113 | t.Errorf("VariableStruct.String() = %v, want %v", got, tt.want) 114 | } 115 | }) 116 | } 117 | } 118 | 119 | func TestParseCauseOfTransmission(t *testing.T) { 120 | type args struct { 121 | b byte 122 | } 123 | tests := []struct { 124 | name string 125 | args args 126 | want CauseOfTransmission 127 | }{ 128 | {"no test and neg", args{0x01}, CauseOfTransmission{Cause: Periodic}}, 129 | {"with test", args{0x81}, CauseOfTransmission{Cause: Periodic, IsTest: true}}, 130 | {"with neg", args{0x41}, CauseOfTransmission{Cause: Periodic, IsNegative: true}}, 131 | {"with test and neg", args{0xc1}, CauseOfTransmission{Cause: Periodic, IsTest: true, IsNegative: true}}, 132 | } 133 | for _, tt := range tests { 134 | t.Run(tt.name, func(t *testing.T) { 135 | if got := ParseCauseOfTransmission(tt.args.b); !reflect.DeepEqual(got, tt.want) { 136 | t.Errorf("ParseCauseOfTransmission() = %v, want %v", got, tt.want) 137 | } 138 | }) 139 | } 140 | } 141 | 142 | func TestCauseOfTransmission_Value(t *testing.T) { 143 | tests := []struct { 144 | name string 145 | this CauseOfTransmission 146 | want byte 147 | }{ 148 | {"no test and neg", CauseOfTransmission{Cause: Periodic}, 0x01}, 149 | {"with test", CauseOfTransmission{Cause: Periodic, IsTest: true}, 0x81}, 150 | {"with neg", CauseOfTransmission{Cause: Periodic, IsNegative: true}, 0x41}, 151 | {"with test and neg", CauseOfTransmission{Cause: Periodic, IsTest: true, IsNegative: true}, 0xc1}, 152 | } 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | if got := tt.this.Value(); got != tt.want { 156 | t.Errorf("CauseOfTransmission.Value() = %v, want %v", got, tt.want) 157 | } 158 | }) 159 | } 160 | } 161 | 162 | func TestCauseOfTransmission_String(t *testing.T) { 163 | tests := []struct { 164 | name string 165 | this CauseOfTransmission 166 | want string 167 | }{ 168 | {"no test and neg", CauseOfTransmission{Cause: Periodic}, "COT"}, 169 | {"with test", CauseOfTransmission{Cause: Periodic, IsTest: true}, "COT"}, 170 | {"with neg", CauseOfTransmission{Cause: Periodic, IsNegative: true}, "COT"}, 171 | {"with test and neg", CauseOfTransmission{Cause: Periodic, IsTest: true, IsNegative: true}, "COT"}, 172 | } 173 | for _, tt := range tests { 174 | t.Run(tt.name, func(t *testing.T) { 175 | if got := tt.this.String(); got != tt.want { 176 | t.Errorf("CauseOfTransmission.String() = %v, want %v", got, tt.want) 177 | } 178 | }) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /asdu/information.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | // about information object 应用服务数据单元 - 信息对象 8 | 9 | // InfoObjAddr is the information object address. 10 | // See companion standard 101, subclass 7.2.5. 11 | // The width is controlled by Params.InfoObjAddrSize. 12 | // <0>: 无关的信息对象地址 13 | // - width 1: <1..255> 14 | // - width 2: <1..65535> 15 | // - width 3: <1..16777215> 16 | type InfoObjAddr uint 17 | 18 | // InfoObjAddrIrrelevant Zero means that the information object address is irrelevant. 19 | const InfoObjAddrIrrelevant InfoObjAddr = 0 20 | 21 | // SinglePoint is a measured value of a switch. 22 | // See companion standard 101, subclass 7.2.6.1. 23 | type SinglePoint byte 24 | 25 | // SinglePoint defined 26 | const ( 27 | SPIOff SinglePoint = iota // 关 28 | SPIOn // 开 29 | ) 30 | 31 | // Value single point to byte 32 | func (sf SinglePoint) Value() byte { 33 | return byte(sf & 0x01) 34 | } 35 | 36 | // DoublePoint is a measured value of a determination aware switch. 37 | // See companion standard 101, subclass 7.2.6.2. 38 | type DoublePoint byte 39 | 40 | // DoublePoint defined 41 | const ( 42 | DPIIndeterminateOrIntermediate DoublePoint = iota // 不确定或中间状态 43 | DPIDeterminedOff // 确定状态开 44 | DPIDeterminedOn // 确定状态关 45 | DPIIndeterminate // 不确定或中间状态 46 | ) 47 | 48 | // Value double point to byte 49 | func (sf DoublePoint) Value() byte { 50 | return byte(sf & 0x03) 51 | } 52 | 53 | // QualityDescriptor Quality descriptor flags attribute measured values. 54 | // See companion standard 101, subclass 7.2.6.3. 55 | type QualityDescriptor byte 56 | 57 | // QualityDescriptor defined. 58 | const ( 59 | // QDSOverflow marks whether the value is beyond a predefined range. 60 | QDSOverflow QualityDescriptor = 1 << iota 61 | _ // reserve 62 | _ // reserve 63 | _ // reserve 64 | // QDSBlocked flags that the value is blocked for transmission; the 65 | // value remains in the state that was acquired before it was blocked. 66 | QDSBlocked 67 | // QDSSubstituted flags that the value was provided by the input of 68 | // an operator (dispatcher) instead of an automatic source. 69 | QDSSubstituted 70 | // QDSNotTopical flags that the most recent update was unsuccessful. 71 | QDSNotTopical 72 | // QDSInvalid flags that the value was incorrectly acquired. 73 | QDSInvalid 74 | 75 | // QDSGood means no flags, no problems. 76 | QDSGood QualityDescriptor = 0 77 | ) 78 | 79 | //QualityDescriptorProtection Quality descriptor Protection Equipment flags attribute. 80 | // See companion standard 101, subclass 7.2.6.4. 81 | type QualityDescriptorProtection byte 82 | 83 | // QualityDescriptorProtection defined. 84 | const ( 85 | _ QualityDescriptorProtection = 1 << iota // reserve 86 | _ // reserve 87 | _ // reserve 88 | // QDPElapsedTimeInvalid flags that the elapsed time was incorrectly acquired. 89 | QDPElapsedTimeInvalid 90 | // QDPBlocked flags that the value is blocked for transmission; the 91 | // value remains in the state that was acquired before it was blocked. 92 | QDPBlocked 93 | // QDPSubstituted flags that the value was provided by the input of 94 | // an operator (dispatcher) instead of an automatic source. 95 | QDPSubstituted 96 | // QDPNotTopical flags that the most recent update was unsuccessful. 97 | QDPNotTopical 98 | // QDPInvalid flags that the value was incorrectly acquired. 99 | QDPInvalid 100 | 101 | // QDPGood means no flags, no problems. 102 | QDPGood QualityDescriptorProtection = 0 103 | ) 104 | 105 | // StepPosition is a measured value with transient state indication. 106 | // 带瞬变状态指示的测量值,用于变压器步位置或其它步位置的值 107 | // See companion standard 101, subclass 7.2.6.5. 108 | // Val range <-64..63> 109 | // bit[0-5]: <-64..63> 110 | // NOTE: bit6 为符号位 111 | // bit7: 0: 设备未在瞬变状态 1: 设备处于瞬变状态 112 | type StepPosition struct { 113 | Val int 114 | HasTransient bool 115 | } 116 | 117 | // Value returns step position value. 118 | func (sf StepPosition) Value() byte { 119 | p := sf.Val & 0x7f 120 | if sf.HasTransient { 121 | p |= 0x80 122 | } 123 | return byte(p) 124 | } 125 | 126 | // ParseStepPosition parse byte to StepPosition. 127 | func ParseStepPosition(b byte) StepPosition { 128 | step := StepPosition{HasTransient: (b & 0x80) != 0} 129 | if b&0x40 == 0 { 130 | step.Val = int(b & 0x3f) 131 | } else { 132 | step.Val = int(b) | (-1 &^ 0x3f) 133 | } 134 | return step 135 | } 136 | 137 | // Normalize is a 16-bit normalized value in[-1, 1 − 2⁻¹⁵].. 138 | // 规一化值 f归一= 32768 * f真实 / 满码值 139 | // See companion standard 101, subclass 7.2.6.6. 140 | type Normalize int16 141 | 142 | // Float64 returns the value in [-1, 1 − 2⁻¹⁵]. 143 | func (sf Normalize) Float64() float64 { 144 | return float64(sf) / 32768 145 | } 146 | 147 | // BinaryCounterReading is binary counter reading 148 | // See companion standard 101, subclass 7.2.6.9. 149 | // CounterReading: 计数器读数 [bit0...bit31] 150 | // SeqNumber: 顺序记法 [bit32...bit40] 151 | // SQ: 顺序号 [bit32...bit36] 152 | // CY: 进位 [bit37] 153 | // CA: 计数量被调整 154 | // IV: 无效 155 | type BinaryCounterReading struct { 156 | CounterReading int32 157 | SeqNumber byte 158 | HasCarry bool 159 | IsAdjusted bool 160 | IsInvalid bool 161 | } 162 | 163 | // SingleEvent is single event 164 | // See companion standard 101, subclass 7.2.6.10. 165 | type SingleEvent byte 166 | 167 | // SingleEvent dSequenceNotationefined 168 | const ( 169 | SEIndeterminateOrIntermediate SingleEvent = iota // 不确定或中间状态 170 | SEDeterminedOff // 确定状态开 171 | SEDeterminedOn // 确定状态关 172 | SEIndeterminate // 不确定或中间状态 173 | ) 174 | 175 | // StartEvent Start event protection 176 | type StartEvent byte 177 | 178 | // StartEvent defined 179 | // See companion standard 101, subclass 7.2.6.11. 180 | const ( 181 | SEPGeneralStart StartEvent = 1 << iota // 总启动 182 | SEPStartL1 // A相保护启动 183 | SEPStartL2 // B相保护启动 184 | SEPStartL3 // C相保护启动 185 | SEPStartEarthCurrent // 接地电流保护启动 186 | SEPStartReverseDirection // 反向保护启动 187 | // other reserved 188 | ) 189 | 190 | // OutputCircuitInfo output command information 191 | // See companion standard 101, subclass 7.2.6.12. 192 | type OutputCircuitInfo byte 193 | 194 | // OutputCircuitInfo defined 195 | const ( 196 | OCIGeneralCommand OutputCircuitInfo = 1 << iota // 总命令输出至输出电路 197 | OCICommandL1 // A 相保护命令输出至输出电路 198 | OCICommandL2 // B 相保护命令输出至输出电路 199 | OCICommandL3 // C 相保护命令输出至输出电路 200 | // other reserved 201 | ) 202 | 203 | // FBPTestWord test special value 204 | // See companion standard 101, subclass 7.2.6.14. 205 | const FBPTestWord uint16 = 0x55aa 206 | 207 | // SingleCommand Single command 208 | // See companion standard 101, subclass 7.2.6.15. 209 | type SingleCommand byte 210 | 211 | // SingleCommand defined 212 | const ( 213 | SCOOn SingleCommand = iota 214 | SCOOff 215 | ) 216 | 217 | // DoubleCommand double command 218 | // See companion standard 101, subclass 7.2.6.16. 219 | type DoubleCommand byte 220 | 221 | // DoubleCommand defined 222 | const ( 223 | DCONotAllow0 DoubleCommand = iota 224 | DCOOn 225 | DCOOff 226 | DCONotAllow3 227 | ) 228 | 229 | // StepCommand step command 230 | // See companion standard 101, subclass 7.2.6.17. 231 | type StepCommand byte 232 | 233 | // StepCommand defined 234 | const ( 235 | SCONotAllow0 StepCommand = iota 236 | SCOStepDown 237 | SCOStepUP 238 | SCONotAllow3 239 | ) 240 | 241 | // COICause Initialization reason 242 | // See companion standard 101, subclass 7.2.6.21. 243 | type COICause byte 244 | 245 | // COICause defined 246 | // 0: 当地电源合上 247 | // 1: 当地手动复位 248 | // 2: 远方复位 249 | // <3..31>: 本配讨标准备的标准定义保留 250 | // <32...127>: 为特定使用保留 251 | const ( 252 | COILocalPowerOn COICause = iota 253 | COILocalHandReset 254 | COIRemoteReset 255 | ) 256 | 257 | // CauseOfInitial cause of initial 258 | // Cause: see COICause 259 | // IsLocalChange: false - 未改变当地参数的初始化 260 | // true - 改变当地参数后的初始化 261 | type CauseOfInitial struct { 262 | Cause COICause 263 | IsLocalChange bool 264 | } 265 | 266 | // ParseCauseOfInitial parse byte to cause of initial 267 | func ParseCauseOfInitial(b byte) CauseOfInitial { 268 | return CauseOfInitial{ 269 | Cause: COICause(b & 0x7f), 270 | IsLocalChange: b&0x80 == 0x80, 271 | } 272 | } 273 | 274 | // Value CauseOfInitial to byte 275 | func (sf CauseOfInitial) Value() byte { 276 | if sf.IsLocalChange { 277 | return byte(sf.Cause | 0x80) 278 | } 279 | return byte(sf.Cause) 280 | } 281 | 282 | // QualifierOfInterrogation Qualifier Of Interrogation 283 | // See companion standard 101, subclass 7.2.6.22. 284 | type QualifierOfInterrogation byte 285 | 286 | // QualifierOfInterrogation defined 287 | const ( 288 | // <1..19>: 为标准定义保留 289 | QOIStation QualifierOfInterrogation = 20 + iota // interrogated by station interrogation 290 | QOIGroup1 // interrogated by group 1 interrogation 291 | QOIGroup2 // interrogated by group 2 interrogation 292 | QOIGroup3 // interrogated by group 3 interrogation 293 | QOIGroup4 // interrogated by group 4 interrogation 294 | QOIGroup5 // interrogated by group 5 interrogation 295 | QOIGroup6 // interrogated by group 6 interrogation 296 | QOIGroup7 // interrogated by group 7 interrogation 297 | QOIGroup8 // interrogated by group 8 interrogation 298 | QOIGroup9 // interrogated by group 9 interrogation 299 | QOIGroup10 // interrogated by group 10 interrogation 300 | QOIGroup11 // interrogated by group 11 interrogation 301 | QOIGroup12 // interrogated by group 12 interrogation 302 | QOIGroup13 // interrogated by group 13 interrogation 303 | QOIGroup14 // interrogated by group 14 interrogation 304 | QOIGroup15 // interrogated by group 15 interrogation 305 | QOIGroup16 // interrogated by group 16 interrogation 306 | 307 | // <37..63>:为标准定义保留 308 | // <64..255>: 为特定使用保留 309 | 310 | // 0:未使用 311 | QOIUnused QualifierOfInterrogation = 0 312 | ) 313 | 314 | // QCCRequest 请求 [bit0...bit5] 315 | // See companion standard 101, subclass 7.2.6.23. 316 | type QCCRequest byte 317 | 318 | // QCCFreeze 冻结 [bit6,bit7] 319 | // See companion standard 101, subclass 7.2.6.23. 320 | type QCCFreeze byte 321 | 322 | // QCCRequest and QCCFreeze defined 323 | const ( 324 | QCCUnused QCCRequest = iota 325 | QCCGroup1 326 | QCCGroup2 327 | QCCGroup3 328 | QCCGroup4 329 | QCCTotal 330 | // <6..31>: 为标准定义 331 | // <32..63>: 为特定使用保留 332 | QCCFrzRead QCCFreeze = 0x00 // 读(无冻结或复位) 333 | QCCFrzFreezeNoReset QCCFreeze = 0x40 // 计数量冻结不带复位(被冻结的值为累计量) 334 | QCCFrzFreezeReset QCCFreeze = 0x80 // 计数量冻结带复位(被冻结的值为增量信息) 335 | QCCFrzReset QCCFreeze = 0xc0 // 计数量复位 336 | ) 337 | 338 | // QualifierCountCall 计数量召唤命令限定词 339 | // See companion standard 101, subclass 7.2.6.23. 340 | type QualifierCountCall struct { 341 | Request QCCRequest 342 | Freeze QCCFreeze 343 | } 344 | 345 | // ParseQualifierCountCall parse byte to QualifierCountCall 346 | func ParseQualifierCountCall(b byte) QualifierCountCall { 347 | return QualifierCountCall{ 348 | Request: QCCRequest(b & 0x3f), 349 | Freeze: QCCFreeze(b & 0xc0), 350 | } 351 | } 352 | 353 | // Value QualifierCountCall to byte 354 | func (sf QualifierCountCall) Value() byte { 355 | return byte(sf.Request&0x3f) | byte(sf.Freeze&0xc0) 356 | } 357 | 358 | // QPMCategory 测量参数类别 359 | type QPMCategory byte 360 | 361 | // QPMCategory defined 362 | const ( 363 | QPMUnused QPMCategory = iota // 0: not used 364 | QPMThreshold // 1: threshold value 365 | QPMSmoothing // 2: smoothing factor (filter time constant) 366 | QPMLowLimit // 3: low limit for transmission of measured values 367 | QPMHighLimit // 4: high limit for transmission of measured values 368 | 369 | // 5‥31: reserved for standard definitions of sf companion standard (compatible range) 370 | // 32‥63: reserved for special use (private range) 371 | 372 | QPMChangeFlag QPMCategory = 0x40 // bit6 marks local parameter change 当地参数改变 373 | QPMInOperationFlag QPMCategory = 0x80 // bit7 marks parameter operation 参数在运行 374 | ) 375 | 376 | // QualifierOfParameterMV Qualifier Of Parameter Of Measured Values 测量值参数限定词 377 | // See companion standard 101, subclass 7.2.6.24. 378 | // QPMCategory : [bit0...bit5] 参数类型 379 | // IsChange : [bit6]当地参数改变,false - 未改变,true - 改变 380 | // IsInOperation : [bit7] 参数在运行,false - 运行, true - 不在运行 381 | type QualifierOfParameterMV struct { 382 | Category QPMCategory 383 | IsChange bool 384 | IsInOperation bool 385 | } 386 | 387 | // ParseQualifierOfParamMV parse byte to QualifierOfParameterMV 388 | func ParseQualifierOfParamMV(b byte) QualifierOfParameterMV { 389 | return QualifierOfParameterMV{ 390 | Category: QPMCategory(b & 0x3f), 391 | IsChange: b&0x40 == 0x40, 392 | IsInOperation: b&0x80 == 0x80, 393 | } 394 | } 395 | 396 | // Value QualifierOfParameterMV to byte 397 | func (sf QualifierOfParameterMV) Value() byte { 398 | v := byte(sf.Category) & 0x3f 399 | if sf.IsChange { 400 | v |= 0x40 401 | } 402 | if sf.IsInOperation { 403 | v |= 0x80 404 | } 405 | return v 406 | } 407 | 408 | // QualifierOfParameterAct Qualifier Of Parameter Activation 参数激活限定词 409 | // See companion standard 101, subclass 7.2.6.25. 410 | type QualifierOfParameterAct byte 411 | 412 | // QualifierOfParameterAct defined 413 | const ( 414 | QPAUnused QualifierOfParameterAct = iota 415 | // 激活/停止激活这之前装载的参数(信息对象地址=0) 416 | QPADeActPrevLoadedParameter 417 | // 激活/停止激活所寻址信息对象的参数 418 | QPADeActObjectParameter 419 | // 激活/停止激活所寻址的持续循环或周期传输的信息对象 420 | QPADeActObjectTransmission 421 | // 4‥127: reserved for standard definitions of sf companion standard (compatible range) 422 | // 128‥255: reserved for special use (private range) 423 | ) 424 | 425 | // QOCQual the qualifier of qual. 426 | // See companion standard 101, subclass 7.2.6.26. 427 | type QOCQual byte 428 | 429 | // QOCQual defined 430 | const ( 431 | // 0: no additional definition 432 | // 无另外的定义 433 | QOCNoAdditionalDefinition QOCQual = iota 434 | // 1: short pulse duration (circuit-breaker), duration determined by a system parameter in the outstation 435 | // 短脉冲持续时间(断路器),持续时间由被控站内的系统参数所确定 436 | QOCShortPulseDuration 437 | // 2: long pulse duration, duration determined by a system parameter in the outstation 438 | // 长脉冲持续时间,持续时间由被控站内的系统参数所确定 439 | QOCLongPulseDuration 440 | // 3: persistent output 441 | // 持续输出 442 | QOCPersistentOutput 443 | // 4‥8: reserved for standard definitions of sf companion standard 444 | // 9‥15: reserved for the selection of other predefined functions 445 | // 16‥31: reserved for special use (private range) 446 | ) 447 | 448 | // QualifierOfCommand is a qualifier of command. 命令限定词 449 | // See companion standard 101, subclass 7.2.6.26. 450 | // See section 5, subclass 6.8. 451 | // InSelect: true - selects, false - executes. 452 | type QualifierOfCommand struct { 453 | Qual QOCQual 454 | InSelect bool 455 | } 456 | 457 | // ParseQualifierOfCommand parse byte to QualifierOfCommand 458 | func ParseQualifierOfCommand(b byte) QualifierOfCommand { 459 | return QualifierOfCommand{ 460 | Qual: QOCQual((b >> 2) & 0x1f), 461 | InSelect: b&0x80 == 0x80, 462 | } 463 | } 464 | 465 | // Value QualifierOfCommand to byte 466 | func (sf QualifierOfCommand) Value() byte { 467 | v := (byte(sf.Qual) & 0x1f) << 2 468 | if sf.InSelect { 469 | v |= 0x80 470 | } 471 | return v 472 | } 473 | 474 | // QualifierOfResetProcessCmd 复位进程命令限定词 475 | // See companion standard 101, subclass 7.2.6.27. 476 | type QualifierOfResetProcessCmd byte 477 | 478 | // QualifierOfResetProcessCmd defined 479 | const ( 480 | // 未采用 481 | QRPUnused QualifierOfResetProcessCmd = iota 482 | // 进程的总复位 483 | QPRGeneralRest 484 | // 复位事件缓冲区等待处理的带时标的信息 485 | QPRResetPendingInfoWithTimeTag 486 | // <3..127>: 为标准保留 487 | //<128..255>: 为特定使用保留 488 | ) 489 | 490 | /* 491 | TODO: file 文件相关未定义 492 | */ 493 | 494 | // QOSQual is the qualifier of a set-point command qual. 495 | // See companion standard 101, subclass 7.2.6.39. 496 | // 0: default 497 | // 0‥63: reserved for standard definitions of sf companion standard (compatible range) 498 | // 64‥127: reserved for special use (private range) 499 | type QOSQual uint 500 | 501 | // QualifierOfSetpointCmd is a qualifier of command. 设定命令限定词 502 | // See section 5, subclass 6.8. 503 | // InSelect: true - selects, false - executes. 504 | type QualifierOfSetpointCmd struct { 505 | Qual QOSQual 506 | InSelect bool 507 | } 508 | 509 | // ParseQualifierOfSetpointCmd parse byte to QualifierOfSetpointCmd 510 | func ParseQualifierOfSetpointCmd(b byte) QualifierOfSetpointCmd { 511 | return QualifierOfSetpointCmd{ 512 | Qual: QOSQual(b & 0x7f), 513 | InSelect: b&0x80 == 0x80, 514 | } 515 | } 516 | 517 | // Value QualifierOfSetpointCmd to byte 518 | func (sf QualifierOfSetpointCmd) Value() byte { 519 | v := byte(sf.Qual) & 0x7f 520 | if sf.InSelect { 521 | v |= 0x80 522 | } 523 | return v 524 | } 525 | 526 | // StatusAndStatusChangeDetection 状态和状态变位检出 527 | // See companion standard 101, subclass 7.2.6.40. 528 | type StatusAndStatusChangeDetection uint32 529 | -------------------------------------------------------------------------------- /asdu/information_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "math" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestSinglePoint_Value(t *testing.T) { 10 | tests := []struct { 11 | name string 12 | this SinglePoint 13 | want byte 14 | }{ 15 | {"off", SPIOff, 0x00}, 16 | {"on", SPIOn, 0x01}, 17 | } 18 | for _, tt := range tests { 19 | t.Run(tt.name, func(t *testing.T) { 20 | if got := tt.this.Value(); got != tt.want { 21 | t.Errorf("SinglePoint.Value() = %v, want %v", got, tt.want) 22 | } 23 | }) 24 | } 25 | } 26 | 27 | func TestDoublePoint_Value(t *testing.T) { 28 | tests := []struct { 29 | name string 30 | this DoublePoint 31 | want byte 32 | }{ 33 | {"IndeterminateOrIntermediate", DPIIndeterminateOrIntermediate, 0x00}, 34 | {"DeterminedOff", DPIDeterminedOff, 0x01}, 35 | {"DeterminedOn", DPIDeterminedOn, 0x02}, 36 | {"Indeterminate", DPIIndeterminate, 0x03}, 37 | } 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | if got := tt.this.Value(); got != tt.want { 41 | t.Errorf("DoublePoint.Value() = %v, want %v", got, tt.want) 42 | } 43 | }) 44 | } 45 | } 46 | 47 | func TestParseStepPosition(t *testing.T) { 48 | type args struct { 49 | value byte 50 | } 51 | tests := []struct { 52 | name string 53 | args args 54 | want StepPosition 55 | }{ 56 | {"值0xc0 处于瞬变状态", args{0xc0}, StepPosition{-64, true}}, 57 | {"值0x40 未在瞬变状态", args{0x40}, StepPosition{-64, false}}, 58 | {"值0x87 处于瞬变状态", args{0x87}, StepPosition{0x07, true}}, 59 | {"值0x07 未在瞬变状态", args{0x07}, StepPosition{0x07, false}}, 60 | } 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | if got := ParseStepPosition(tt.args.value); got != tt.want { 64 | t.Errorf("NewStepPos() = %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestStepPosition_Value(t *testing.T) { 71 | for _, HasTransient := range []bool{false, true} { 72 | for value := -64; value <= 63; value++ { 73 | got := ParseStepPosition(StepPosition{value, HasTransient}.Value()) 74 | if got.Val != value || got.HasTransient != HasTransient { 75 | t.Errorf("ParseStepPosition(StepPosition(%d, %t).Value()) = StepPosition(%d, %t)", value, HasTransient, got.Val, got.HasTransient) 76 | } 77 | } 78 | } 79 | } 80 | 81 | // TestNormal tests the full value range. 82 | func TestNormal(t *testing.T) { 83 | v := Normalize(-1 << 15) 84 | last := v.Float64() 85 | if last != -1 { 86 | t.Errorf("%#04x: got %f, want -1", uint16(v), last) 87 | } 88 | 89 | for v != 1<<15-1 { 90 | v++ 91 | got := v.Float64() 92 | if got <= last || got >= 1 { 93 | t.Errorf("%#04x: got %f (%#04x was %f)", uint16(v), got, uint16(v-1), last) 94 | } 95 | last = got 96 | } 97 | } 98 | 99 | func TestNormalize_Float64(t *testing.T) { 100 | min := float64(-1) 101 | for v := math.MinInt16; v < math.MaxInt16; v++ { 102 | got := Normalize(v).Float64() 103 | if got < min || got >= 1 { 104 | t.Errorf("%#04x: got %f (%#04x was %f)", uint16(v), got, uint16(v-1), min) 105 | } 106 | min = got 107 | } 108 | } 109 | 110 | func TestParseQualifierOfCmd(t *testing.T) { 111 | type args struct { 112 | b byte 113 | } 114 | tests := []struct { 115 | name string 116 | args args 117 | want QualifierOfCommand 118 | }{ 119 | {"with selects", args{0x84}, QualifierOfCommand{1, true}}, 120 | {"with executes", args{0x0c}, QualifierOfCommand{3, false}}, 121 | } 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | if got := ParseQualifierOfCommand(tt.args.b); !reflect.DeepEqual(got, tt.want) { 125 | t.Errorf("ParseQualifierOfCommand() = %v, want %v", got, tt.want) 126 | } 127 | }) 128 | } 129 | } 130 | 131 | func TestParseQualifierOfSetpointCmd(t *testing.T) { 132 | type args struct { 133 | b byte 134 | } 135 | tests := []struct { 136 | name string 137 | args args 138 | want QualifierOfSetpointCmd 139 | }{ 140 | {"with selects", args{0x87}, QualifierOfSetpointCmd{7, true}}, 141 | {"with executes", args{0x07}, QualifierOfSetpointCmd{7, false}}, 142 | } 143 | for _, tt := range tests { 144 | t.Run(tt.name, func(t *testing.T) { 145 | if got := ParseQualifierOfSetpointCmd(tt.args.b); !reflect.DeepEqual(got, tt.want) { 146 | t.Errorf("ParseQualifierOfSetpointCmd() = %v, want %v", got, tt.want) 147 | } 148 | }) 149 | } 150 | } 151 | 152 | func TestQualifierOfCmd_Value(t *testing.T) { 153 | type fields struct { 154 | CmdQ QOCQual 155 | InExec bool 156 | } 157 | tests := []struct { 158 | name string 159 | fields fields 160 | want byte 161 | }{ 162 | // TODO: Add test cases. 163 | } 164 | for _, tt := range tests { 165 | t.Run(tt.name, func(t *testing.T) { 166 | this := QualifierOfCommand{ 167 | Qual: tt.fields.CmdQ, 168 | InSelect: tt.fields.InExec, 169 | } 170 | if got := this.Value(); got != tt.want { 171 | t.Errorf("QualifierOfCommand.Value() = %v, want %v", got, tt.want) 172 | } 173 | }) 174 | } 175 | } 176 | 177 | func TestQualifierOfSetpointCmd_Value(t *testing.T) { 178 | type fields struct { 179 | CmdS QOSQual 180 | InExec bool 181 | } 182 | tests := []struct { 183 | name string 184 | fields fields 185 | want byte 186 | }{ 187 | // TODO: Add test cases. 188 | } 189 | for _, tt := range tests { 190 | t.Run(tt.name, func(t *testing.T) { 191 | this := QualifierOfSetpointCmd{ 192 | Qual: tt.fields.CmdS, 193 | InSelect: tt.fields.InExec, 194 | } 195 | if got := this.Value(); got != tt.want { 196 | t.Errorf("QualifierOfSetpointCmd.Value() = %v, want %v", got, tt.want) 197 | } 198 | }) 199 | } 200 | } 201 | 202 | func TestParseQualifierOfParam(t *testing.T) { 203 | type args struct { 204 | b byte 205 | } 206 | tests := []struct { 207 | name string 208 | args args 209 | want QualifierOfParameterMV 210 | }{ 211 | // TODO: Add test cases. 212 | } 213 | for _, tt := range tests { 214 | t.Run(tt.name, func(t *testing.T) { 215 | if got := ParseQualifierOfParamMV(tt.args.b); !reflect.DeepEqual(got, tt.want) { 216 | t.Errorf("ParseQualifierOfParamMV() = %v, want %v", got, tt.want) 217 | } 218 | }) 219 | } 220 | } 221 | 222 | func TestQualifierOfParam_Value(t *testing.T) { 223 | type fields struct { 224 | ParamQ QPMCategory 225 | IsChange bool 226 | IsInOperation bool 227 | } 228 | tests := []struct { 229 | name string 230 | fields fields 231 | want byte 232 | }{ 233 | // TODO: Add test cases. 234 | } 235 | for _, tt := range tests { 236 | t.Run(tt.name, func(t *testing.T) { 237 | this := QualifierOfParameterMV{ 238 | Category: tt.fields.ParamQ, 239 | IsChange: tt.fields.IsChange, 240 | IsInOperation: tt.fields.IsInOperation, 241 | } 242 | if got := this.Value(); got != tt.want { 243 | t.Errorf("QualifierOfParameterMV.Value() = %v, want %v", got, tt.want) 244 | } 245 | }) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /asdu/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "net" 9 | ) 10 | 11 | // Connect interface 12 | type Connect interface { 13 | Params() *Params 14 | Send(a *ASDU) error 15 | UnderlyingConn() net.Conn 16 | } 17 | -------------------------------------------------------------------------------- /asdu/msys.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | // 在监视方向系统信息的应用服务数据单元 8 | 9 | // EndOfInitialization send a type identification [M_EI_NA_1],初始化结束,只有单个信息对象(SQ = 0) 10 | // [M_EI_NA_1] See companion standard 101,subclass 7.3.3.1 11 | // 传送原因(coa)用于 12 | // 监视方向: 13 | // <4> := 被初始化 14 | func EndOfInitialization(c Connect, coa CauseOfTransmission, ca CommonAddr, ioa InfoObjAddr, coi CauseOfInitial) error { 15 | if err := c.Params().Valid(); err != nil { 16 | return err 17 | } 18 | 19 | coa.Cause = Initialized 20 | u := NewASDU(c.Params(), Identifier{ 21 | M_EI_NA_1, 22 | VariableStruct{IsSequence: false, Number: 1}, 23 | coa, 24 | 0, 25 | ca, 26 | }) 27 | 28 | if err := u.AppendInfoObjAddr(ioa); err != nil { 29 | return err 30 | } 31 | u.AppendBytes(coi.Value()) 32 | return c.Send(u) 33 | } 34 | 35 | // GetEndOfInitialization get GetEndOfInitialization for asdu when the identification [M_EI_NA_1] 36 | func (sf *ASDU) GetEndOfInitialization() (InfoObjAddr, CauseOfInitial) { 37 | return sf.DecodeInfoObjAddr(), ParseCauseOfInitial(sf.infoObj[0]) 38 | } 39 | -------------------------------------------------------------------------------- /asdu/msys_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestEndOfInitialization(t *testing.T) { 9 | type args struct { 10 | c Connect 11 | coa CauseOfTransmission 12 | ca CommonAddr 13 | ioa InfoObjAddr 14 | coi CauseOfInitial 15 | } 16 | tests := []struct { 17 | name string 18 | args args 19 | wantErr bool 20 | }{ 21 | { 22 | "M_EI_NA_1", 23 | args{ 24 | newConn([]byte{byte(M_EI_NA_1), 0x01, 0x04, 0x00, 0x34, 0x12, 25 | 0x90, 0x78, 0x56, 0x01}, t), 26 | CauseOfTransmission{Cause: Initialized}, 27 | 0x1234, 28 | 0x567890, 29 | CauseOfInitial{COILocalHandReset, false}}, 30 | false, 31 | }, 32 | } 33 | for _, tt := range tests { 34 | t.Run(tt.name, func(t *testing.T) { 35 | if err := EndOfInitialization(tt.args.c, tt.args.coa, tt.args.ca, tt.args.ioa, tt.args.coi); (err != nil) != tt.wantErr { 36 | t.Errorf("EndOfInitialization() error = %v, wantErr %v", err, tt.wantErr) 37 | } 38 | }) 39 | } 40 | } 41 | 42 | func TestASDU_GetEndOfInitialization(t *testing.T) { 43 | type fields struct { 44 | Params *Params 45 | infoObj []byte 46 | } 47 | tests := []struct { 48 | name string 49 | fields fields 50 | want InfoObjAddr 51 | want1 CauseOfInitial 52 | }{ 53 | { 54 | "M_EI_NA_1", 55 | fields{ParamsWide, []byte{0x90, 0x78, 0x56, 0x01}}, 56 | 0x567890, 57 | CauseOfInitial{COILocalHandReset, false}, 58 | }, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | this := &ASDU{ 63 | Params: tt.fields.Params, 64 | infoObj: tt.fields.infoObj, 65 | } 66 | got, got1 := this.GetEndOfInitialization() 67 | if got != tt.want { 68 | t.Errorf("ASDU.GetEndOfInitialization() got = %v, want %v", got, tt.want) 69 | } 70 | if !reflect.DeepEqual(got1, tt.want1) { 71 | t.Errorf("ASDU.GetEndOfInitialization() got1 = %v, want %v", got1, tt.want1) 72 | } 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /asdu/time.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package asdu 6 | 7 | import ( 8 | "encoding/binary" 9 | "time" 10 | ) 11 | 12 | // CP56Time2a , CP24Time2a, CP16Time2a 13 | // | Milliseconds(D7--D0) | Milliseconds = 0-59999 14 | // | Milliseconds(D15--D8) | 15 | // | IV(D7) RES1(D6) Minutes(D5--D0) | Minutes = 1-59, IV = invalid,0 = valid, 1 = invalid 16 | // | SU(D7) RES2(D6-D5) Hours(D4--D0) | Hours = 0-23, SU = summer Time,0 = standard time, 1 = summer time, 17 | // | DayOfWeek(D7--D5) DayOfMonth(D4--D0)| DayOfMonth = 1-31 DayOfWeek = 1-7 18 | // | RES3(D7--D4) Months(D3--D0) | Months = 1-12 19 | // | RES4(D7) Year(D6--D0) | Year = 0-99 20 | 21 | // CP56Time2a time to CP56Time2a 22 | func CP56Time2a(t time.Time, loc *time.Location) []byte { 23 | if loc == nil { 24 | loc = time.UTC 25 | } 26 | ts := t.In(loc) 27 | msec := ts.Nanosecond()/int(time.Millisecond) + ts.Second()*1000 28 | return []byte{byte(msec), byte(msec >> 8), byte(ts.Minute()), byte(ts.Hour()), 29 | byte(ts.Weekday()<<5) | byte(ts.Day()), byte(ts.Month()), byte(ts.Year() - 2000)} 30 | } 31 | 32 | // ParseCP56Time2a 7个八位位组二进制时间,建议所有时标采用UTC,读7个字节,返回时间 33 | // The year is assumed to be in the 20th century. 34 | // See IEC 60870-5-4 § 6.8 and IEC 60870-5-101 second edition § 7.2.6.18. 35 | func ParseCP56Time2a(bytes []byte, loc *time.Location) time.Time { 36 | if len(bytes) < 7 || bytes[2]&0x80 == 0x80 { 37 | return time.Time{} 38 | } 39 | 40 | x := int(binary.LittleEndian.Uint16(bytes)) 41 | msec := x % 1000 42 | sec := x / 1000 43 | min := int(bytes[2] & 0x3f) 44 | hour := int(bytes[3] & 0x1f) 45 | day := int(bytes[4] & 0x1f) 46 | month := time.Month(bytes[5] & 0x0f) 47 | year := 2000 + int(bytes[6]&0x7f) 48 | 49 | nsec := msec * int(time.Millisecond) 50 | if loc == nil { 51 | loc = time.UTC 52 | } 53 | return time.Date(year, month, day, hour, min, sec, nsec, loc) 54 | } 55 | 56 | // CP24Time2a time to CP56Time2a 3个八位位组二进制时间,建议所有时标采用UTC 57 | // See companion standard 101, subclass 7.2.6.19. 58 | func CP24Time2a(t time.Time, loc *time.Location) []byte { 59 | if loc == nil { 60 | loc = time.UTC 61 | } 62 | ts := t.In(loc) 63 | msec := ts.Nanosecond()/int(time.Millisecond) + ts.Second()*1000 64 | return []byte{byte(msec), byte(msec >> 8), byte(ts.Minute())} 65 | } 66 | 67 | // ParseCP24Time2a 3个八位位组二进制时间,建议所有时标采用UTC,读3字节,返回一个时间 68 | // See companion standard 101, subclass 7.2.6.19. 69 | func ParseCP24Time2a(bytes []byte, loc *time.Location) time.Time { 70 | if len(bytes) < 3 || bytes[2]&0x80 == 0x80 { 71 | return time.Time{} 72 | } 73 | x := int(binary.LittleEndian.Uint16(bytes)) 74 | msec := x % 1000 75 | sec := (x / 1000) 76 | min := int(bytes[2] & 0x3f) 77 | now := time.Now() 78 | year, month, day := now.Date() 79 | hour, _, _ := now.Clock() 80 | 81 | nsec := msec * int(time.Millisecond) 82 | if loc == nil { 83 | loc = time.UTC 84 | } 85 | val := time.Date(year, month, day, hour, min, sec, nsec, loc) 86 | 87 | ////5 minute rounding - 55 minute span 88 | //if min > currentMin+5 { 89 | // val = val.Add(-time.Hour) 90 | //} 91 | 92 | return val 93 | } 94 | 95 | // CP16Time2a msec to CP16Time2a 2个八位位组二进制时间 96 | // See companion standard 101, subclass 7.2.6.20. 97 | func CP16Time2a(msec uint16) []byte { 98 | return []byte{byte(msec), byte(msec >> 8)} 99 | } 100 | 101 | // ParseCP16Time2a 2个八位位组二进制时间,读2字节,返回一个值 102 | // See companion standard 101, subclass 7.2.6.20. 103 | func ParseCP16Time2a(b []byte) uint16 { 104 | return binary.LittleEndian.Uint16(b) 105 | } 106 | -------------------------------------------------------------------------------- /asdu/time_test.go: -------------------------------------------------------------------------------- 1 | package asdu 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | var ( 10 | tm0 = time.Date(2019, 6, 5, 4, 3, 0, 513000000, time.UTC) 11 | tm0CP56Time2aBytes = []byte{0x01, 0x02, 0x03, 0x04, 0x65, 0x06, 0x13} 12 | tm0CP24Time2aBytes = tm0CP56Time2aBytes[:3] 13 | 14 | tm1 = time.Date(2019, 12, 15, 14, 13, 3, 83000000, time.UTC) 15 | tm1CP56Time2aBytes = []byte{0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x0c, 0x13} 16 | tm1CP24Time2aBytes = tm1CP56Time2aBytes[:3] 17 | ) 18 | 19 | func TestCP56Time2a(t *testing.T) { 20 | type args struct { 21 | t time.Time 22 | loc *time.Location 23 | } 24 | tests := []struct { 25 | name string 26 | args args 27 | want []byte 28 | }{ 29 | {"20190605", args{tm0, nil}, tm0CP56Time2aBytes}, 30 | {"20191215", args{tm1, time.UTC}, tm1CP56Time2aBytes}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := CP56Time2a(tt.args.t, tt.args.loc); !reflect.DeepEqual(got, tt.want) { 35 | t.Errorf("CP56Time2a() = % x, want % x", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | 41 | func TestParseCP56Time2a(t *testing.T) { 42 | type args struct { 43 | bytes []byte 44 | loc *time.Location 45 | } 46 | tests := []struct { 47 | name string 48 | args args 49 | want time.Time 50 | }{ 51 | { 52 | "invalid flag", args{ 53 | []byte{0x01, 0x02, 0x83, 0x04, 0x65, 0x06, 0x13}, 54 | nil}, 55 | time.Time{}, 56 | }, 57 | {"20190605", args{tm0CP56Time2aBytes, nil}, tm0}, 58 | {"20191215", args{tm1CP56Time2aBytes, time.UTC}, tm1}, 59 | } 60 | for _, tt := range tests { 61 | t.Run(tt.name, func(t *testing.T) { 62 | got := ParseCP56Time2a(tt.args.bytes, tt.args.loc) 63 | if !reflect.DeepEqual(got, tt.want) { 64 | t.Errorf("ParseCP56Time2a() = %v, want %v", got, tt.want) 65 | } 66 | }) 67 | } 68 | } 69 | 70 | func TestCP24Time2a(t *testing.T) { 71 | type args struct { 72 | t time.Time 73 | loc *time.Location 74 | } 75 | tests := []struct { 76 | name string 77 | args args 78 | want []byte 79 | }{ 80 | {"3 Minutes 513 Milliseconds", args{tm0, nil}, tm0CP24Time2aBytes}, 81 | {"13 Minutes 3083 Milliseconds", args{tm1, time.UTC}, tm1CP24Time2aBytes}, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | if got := CP24Time2a(tt.args.t, tt.args.loc); !reflect.DeepEqual(got, tt.want) { 86 | t.Errorf("CP24Time2a() = %v, want %v", got, tt.want) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestParseCP24Time2a(t *testing.T) { 93 | type args struct { 94 | bytes []byte 95 | loc *time.Location 96 | } 97 | tests := []struct { 98 | name string 99 | args args 100 | wantMsec int 101 | wantMin int 102 | }{ 103 | { 104 | "invalid flag", 105 | args{[]byte{0x01, 0x02, 0x83}, nil}, 106 | 0, 107 | 0, 108 | }, 109 | { 110 | "3 Minutes 513 Milliseconds", 111 | args{tm0CP24Time2aBytes, nil}, 112 | 513, 113 | 3, 114 | }, 115 | { 116 | "13 Minutes 3083 Milliseconds", 117 | args{tm1CP24Time2aBytes, time.UTC}, 118 | 3083, 119 | 13, 120 | }, 121 | } 122 | for _, tt := range tests { 123 | t.Run(tt.name, func(t *testing.T) { 124 | got := ParseCP24Time2a(tt.args.bytes, tt.args.loc) 125 | msec := (got.Nanosecond()/int(time.Millisecond) + got.Second()*1000) 126 | if msec != tt.wantMsec { 127 | t.Errorf("ParseCP24Time2a() go Millisecond = %v, want %v", msec, tt.wantMsec) 128 | } 129 | if got.Minute() != tt.wantMin { 130 | t.Errorf("ParseCP24Time2a() got Minute = %v, want %v", got.Minute(), tt.wantMin) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func TestCP16Time2a(t *testing.T) { 137 | type args struct { 138 | msec uint16 139 | } 140 | tests := []struct { 141 | name string 142 | args args 143 | want []byte 144 | }{ 145 | {"513 Milliseconds", args{513}, []byte{0x01, 0x02}}, 146 | {"3083 Milliseconds", args{3083}, []byte{0x0b, 0x0c}}, 147 | } 148 | for _, tt := range tests { 149 | t.Run(tt.name, func(t *testing.T) { 150 | if got := CP16Time2a(tt.args.msec); !reflect.DeepEqual(got, tt.want) { 151 | t.Errorf("CP16Time2a() = %v, want %v", got, tt.want) 152 | } 153 | }) 154 | } 155 | } 156 | 157 | func TestParseCP16Time2a(t *testing.T) { 158 | type args struct { 159 | b []byte 160 | } 161 | tests := []struct { 162 | name string 163 | args args 164 | want uint16 165 | }{ 166 | {"513 Milliseconds", args{[]byte{0x01, 0x02}}, 513}, 167 | {"3083 Milliseconds", args{[]byte{0x0b, 0x0c}}, 3083}, 168 | } 169 | for _, tt := range tests { 170 | t.Run(tt.name, func(t *testing.T) { 171 | if got := ParseCP16Time2a(tt.args.b); got != tt.want { 172 | t.Errorf("ParseCP16Time2a() = %v, want %v", got, tt.want) 173 | } 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /clog/clog.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package clog 6 | 7 | import ( 8 | "log" 9 | "os" 10 | "sync/atomic" 11 | ) 12 | 13 | // LogProvider RFC5424 log message levels only Debug Warn and Error 14 | type LogProvider interface { 15 | Critical(format string, v ...interface{}) 16 | Error(format string, v ...interface{}) 17 | Warn(format string, v ...interface{}) 18 | Debug(format string, v ...interface{}) 19 | } 20 | 21 | // Clog 日志内部调试实现 22 | type Clog struct { 23 | provider LogProvider 24 | // is log output enabled,1: enable, 0: disable 25 | has uint32 26 | } 27 | 28 | // NewLogger 创建一个新的日志,采用指定prefix前缀 29 | func NewLogger(prefix string) Clog { 30 | return Clog{ 31 | defaultLogger{ 32 | log.New(os.Stdout, prefix, log.LstdFlags), 33 | }, 34 | 0, 35 | } 36 | } 37 | 38 | // LogMode set enable or disable log output when you has set provider 39 | func (sf *Clog) LogMode(enable bool) { 40 | if enable { 41 | atomic.StoreUint32(&sf.has, 1) 42 | } else { 43 | atomic.StoreUint32(&sf.has, 0) 44 | } 45 | } 46 | 47 | // SetLogProvider set provider provider 48 | func (sf *Clog) SetLogProvider(p LogProvider) { 49 | if p != nil { 50 | sf.provider = p 51 | } 52 | } 53 | 54 | // Critical Log CRITICAL level message. 55 | func (sf Clog) Critical(format string, v ...interface{}) { 56 | if atomic.LoadUint32(&sf.has) == 1 { 57 | sf.provider.Critical(format, v...) 58 | } 59 | } 60 | 61 | // Error Log ERROR level message. 62 | func (sf Clog) Error(format string, v ...interface{}) { 63 | if atomic.LoadUint32(&sf.has) == 1 { 64 | sf.provider.Error(format, v...) 65 | } 66 | } 67 | 68 | // Warn Log WARN level message. 69 | func (sf Clog) Warn(format string, v ...interface{}) { 70 | if atomic.LoadUint32(&sf.has) == 1 { 71 | sf.provider.Warn(format, v...) 72 | } 73 | } 74 | 75 | // Debug Log DEBUG level message. 76 | func (sf Clog) Debug(format string, v ...interface{}) { 77 | if atomic.LoadUint32(&sf.has) == 1 { 78 | sf.provider.Debug(format, v...) 79 | } 80 | } 81 | 82 | // default log 83 | type defaultLogger struct { 84 | *log.Logger 85 | } 86 | 87 | var _ LogProvider = (*defaultLogger)(nil) 88 | 89 | // Critical Log CRITICAL level message. 90 | func (sf defaultLogger) Critical(format string, v ...interface{}) { 91 | sf.Printf("[C]: "+format, v...) 92 | } 93 | 94 | // Error Log ERROR level message. 95 | func (sf defaultLogger) Error(format string, v ...interface{}) { 96 | sf.Printf("[E]: "+format, v...) 97 | } 98 | 99 | // Warn Log WARN level message. 100 | func (sf defaultLogger) Warn(format string, v ...interface{}) { 101 | sf.Printf("[W]: "+format, v...) 102 | } 103 | 104 | // Debug Log DEBUG level message. 105 | func (sf defaultLogger) Debug(format string, v ...interface{}) { 106 | sf.Printf("[D]: "+format, v...) 107 | } 108 | -------------------------------------------------------------------------------- /cs101/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs101 6 | -------------------------------------------------------------------------------- /cs101/ft.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs101 6 | 7 | // 采用FT1.2帧格式 8 | const ( 9 | startVarFrame byte = 0x68 // 长度可变帧启动字符 10 | startFixFrame byte = 0x10 // 长度固定帧启动字符 11 | endFrame byte = 0x16 12 | ) 13 | 14 | // 控制域定义 15 | const ( 16 | 17 | // 启动站到从动站特有 18 | FCV = 1 << 4 // 帧计数有效位 19 | FCB = 1 << 5 // 帧计数位 20 | // 从动站到启动站特有 21 | DFC = 1 << 4 // 数据流控制位 22 | ACD_RES = 1 << 5 // 要求访问位,非平衡ACD,平衡保留 23 | // 启动报文位: 24 | // PRM = 0, 由从动站向启动站传输报文; 25 | // PRM = 1, 由启动站向从动站传输报文 26 | RPM = 1 << 6 27 | RES_DIR = 1 << 7 // 非平衡保留,平衡为方向 28 | 29 | // 由启动站向从动站传输的报文中控制域的功能码(PRM = 1) 30 | FccResetRemoteLink = iota // 复位远方链路 31 | FccResetUserProcess // 复位用户进程 32 | FccBalanceTestLink // 链路测试功能 33 | FccUserDataWithConfirmed // 用户数据,需确认 34 | FccUserDataWithUnconfirmed // 用户数据,无需确认 35 | _ // 保留 36 | _ // 制造厂和用户协商定义 37 | _ // 制造厂和用户协商定义 38 | FccUnbalanceWithRequestBitResponse // 以要求访问位响应 39 | FccLinkStatus // 请求链路状态 40 | FccUnbalanceLevel1UserData // 请求 1 级用户数据 41 | FccUnbalanceLevel2UserData // 请求 2 级用户数据 42 | // 12-13: 备用 43 | // 14-15: 制造厂和用户协商定义 44 | 45 | // 从动站向启动站传输的报文中控制域的功能码(PRM = 0) 46 | FcsConfirmed = iota // 认可: 肯定认可 47 | FcsNConfirmed // 否定认可: 未收到报文,链路忙 48 | _ // 保留 49 | _ // 保留 50 | _ // 保留 51 | _ // 保留 52 | _ // 制造厂和用户协商定义 53 | _ // 制造厂和用户协商定义 54 | FcsUnbalanceResponse // 用户数据 55 | FcsUnbalanceNegativeResponse // 否定认哥: 无所召唤数据 56 | _ // 保留 57 | FcsStatus // 链路状态或要求访问 58 | // 12: 备用 59 | // 13: 制造厂和用户协商定义 60 | // 14: 链路服务未工作 61 | // 15: 链路服务未完成 62 | ) 63 | 64 | // Ft12 ... 65 | type Ft12 struct { 66 | start byte 67 | apduFiledLen byte 68 | ctrl byte 69 | address uint16 70 | checksum byte 71 | end byte 72 | } 73 | -------------------------------------------------------------------------------- /cs104/apci.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "fmt" 9 | 10 | "github.com/thinkgos/go-iecp5/asdu" 11 | ) 12 | 13 | const startFrame byte = 0x68 // 启动字符 14 | 15 | // APDU form Max size 255 16 | // | APCI | ASDU | 17 | // | start | APDU length | control field | ASDU | 18 | // | APDU field size(253) | 19 | // bytes| 1 | 1 | 4 | | 20 | const ( 21 | APCICtlFiledSize = 4 // control filed(4) 22 | 23 | APDUSizeMax = 255 // start(1) + length(1) + control field(4) + ASDU 24 | APDUFieldSizeMax = APCICtlFiledSize + asdu.ASDUSizeMax // control field(4) + ASDU 25 | ) 26 | 27 | // U帧 控制域功能 28 | const ( 29 | uStartDtActive byte = 4 << iota // 启动激活 0x04 30 | uStartDtConfirm // 启动确认 0x08 31 | uStopDtActive // 停止激活 0x10 32 | uStopDtConfirm // 停止确认 0x20 33 | uTestFrActive // 测试激活 0x40 34 | uTestFrConfirm // 测试确认 0x80 35 | ) 36 | 37 | // I帧 含apci和asdu 信息帧.用于编号的信息传输 information 38 | type iAPCI struct { 39 | sendSN, rcvSN uint16 40 | } 41 | 42 | func (sf iAPCI) String() string { 43 | return fmt.Sprintf("I[sendNO: %d, recvNO: %d]", sf.sendSN, sf.rcvSN) 44 | } 45 | 46 | // S帧 只含apci S帧用于主要用确认帧的正确传输,协议称是监视. supervisory 47 | type sAPCI struct { 48 | rcvSN uint16 49 | } 50 | 51 | func (sf sAPCI) String() string { 52 | return fmt.Sprintf("S[recvNO: %d]", sf.rcvSN) 53 | } 54 | 55 | //U帧 只含apci 未编号控制信息 unnumbered 56 | type uAPCI struct { 57 | function byte // bit8 测试确认 58 | } 59 | 60 | func (sf uAPCI) String() string { 61 | var s string 62 | switch sf.function { 63 | case uStartDtActive: 64 | s = "StartDtActive" 65 | case uStartDtConfirm: 66 | s = "StartDtConfirm" 67 | case uStopDtActive: 68 | s = "StopDtActive" 69 | case uStopDtConfirm: 70 | s = "StopDtConfirm" 71 | case uTestFrActive: 72 | s = "TestFrActive" 73 | case uTestFrConfirm: 74 | s = "TestFrConfirm" 75 | default: 76 | s = "Unknown" 77 | } 78 | return fmt.Sprintf("U[function: %s]", s) 79 | } 80 | 81 | // newIFrame 创建I帧 ,返回apdu 82 | func newIFrame(sendSN, RcvSN uint16, asdus []byte) ([]byte, error) { 83 | if len(asdus) > asdu.ASDUSizeMax { 84 | return nil, fmt.Errorf("ASDU filed large than max %d", asdu.ASDUSizeMax) 85 | } 86 | 87 | b := make([]byte, len(asdus)+6) 88 | 89 | b[0] = startFrame 90 | b[1] = byte(len(asdus) + 4) 91 | b[2] = byte(sendSN << 1) 92 | b[3] = byte(sendSN >> 7) 93 | b[4] = byte(RcvSN << 1) 94 | b[5] = byte(RcvSN >> 7) 95 | copy(b[6:], asdus) 96 | 97 | return b, nil 98 | } 99 | 100 | // newSFrame 创建S帧,返回apdu 101 | func newSFrame(RcvSN uint16) []byte { 102 | return []byte{startFrame, 4, 0x01, 0x00, byte(RcvSN << 1), byte(RcvSN >> 7)} 103 | } 104 | 105 | // newUFrame 创建U帧,返回apdu 106 | func newUFrame(which byte) []byte { 107 | return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 108 | } 109 | 110 | // APCI apci 应用规约控制信息 111 | type APCI struct { 112 | start byte 113 | apduFiledLen byte // control + asdu 的长度 114 | ctr1, ctr2, ctr3, ctr4 byte 115 | } 116 | 117 | // return frame type , APCI, remain data 118 | func parse(apdu []byte) (interface{}, []byte) { 119 | apci := APCI{apdu[0], apdu[1], apdu[2], apdu[3], apdu[4], apdu[5]} 120 | if apci.ctr1&0x01 == 0 { 121 | return iAPCI{ 122 | sendSN: uint16(apci.ctr1)>>1 + uint16(apci.ctr2)<<7, 123 | rcvSN: uint16(apci.ctr3)>>1 + uint16(apci.ctr4)<<7, 124 | }, apdu[6:] 125 | } 126 | if apci.ctr1&0x03 == 0x01 { 127 | return sAPCI{ 128 | rcvSN: uint16(apci.ctr3)>>1 + uint16(apci.ctr4)<<7, 129 | }, apdu[6:] 130 | } 131 | // apci.ctrl&0x03 == 0x03 132 | return uAPCI{ 133 | function: apci.ctr1 & 0xfc, 134 | }, apdu[6:] 135 | } 136 | -------------------------------------------------------------------------------- /cs104/apci_test.go: -------------------------------------------------------------------------------- 1 | package cs104 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestIAPCI_String(t *testing.T) { 9 | tests := []struct { 10 | name string 11 | this iAPCI 12 | want string 13 | }{ 14 | {"iFrame", iAPCI{sendSN: 0x02, rcvSN: 0x02}, "I[sendNO: 2, recvNO: 2]"}, 15 | } 16 | for _, tt := range tests { 17 | t.Run(tt.name, func(t *testing.T) { 18 | if got := tt.this.String(); got != tt.want { 19 | t.Errorf("APCI.String() = %v, want %v", got, tt.want) 20 | } 21 | }) 22 | } 23 | } 24 | func TestSAPCI_String(t *testing.T) { 25 | tests := []struct { 26 | name string 27 | this sAPCI 28 | want string 29 | }{ 30 | {"sFrame", sAPCI{rcvSN: 123}, "S[recvNO: 123]"}, 31 | } 32 | for _, tt := range tests { 33 | t.Run(tt.name, func(t *testing.T) { 34 | if got := tt.this.String(); got != tt.want { 35 | t.Errorf("APCI.String() = %v, want %v", got, tt.want) 36 | } 37 | }) 38 | } 39 | } 40 | func TestUAPCI_String(t *testing.T) { 41 | tests := []struct { 42 | name string 43 | this uAPCI 44 | want string 45 | }{ 46 | {"uFrame", uAPCI{function: uStartDtActive}, "U[function: StartDtActive]"}, 47 | } 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | if got := tt.this.String(); got != tt.want { 51 | t.Errorf("APCI.String() = %v, want %v", got, tt.want) 52 | } 53 | }) 54 | } 55 | } 56 | 57 | func Test_newIFrame(t *testing.T) { 58 | type args struct { 59 | asdu []byte 60 | sendSN uint16 61 | RcvSN uint16 62 | } 63 | tests := []struct { 64 | name string 65 | args args 66 | want []byte 67 | wantErr bool 68 | }{ 69 | { 70 | "asdu out of range", 71 | args{asdu: make([]byte, 250)}, 72 | nil, 73 | true, 74 | }, 75 | { 76 | "asdu right", 77 | args{[]byte{0x01, 0x02}, 0x06, 0x07}, 78 | []byte{startFrame, 0x06, 0x0c, 0x00, 0x0e, 0x00, 0x01, 0x02}, 79 | false, 80 | }, 81 | } 82 | for _, tt := range tests { 83 | t.Run(tt.name, func(t *testing.T) { 84 | got, err := newIFrame(tt.args.sendSN, tt.args.RcvSN, tt.args.asdu) 85 | if (err != nil) != tt.wantErr { 86 | t.Errorf("newIFrame() error = %v, wantErr %v", err, tt.wantErr) 87 | return 88 | } 89 | if !reflect.DeepEqual(got, tt.want) { 90 | t.Errorf("newIFrame() = % x, want % x", got, tt.want) 91 | } 92 | }) 93 | } 94 | } 95 | 96 | func Test_newSFrame(t *testing.T) { 97 | type args struct { 98 | RcvSN uint16 99 | } 100 | tests := []struct { 101 | name string 102 | args args 103 | want []byte 104 | }{ 105 | {"", args{0x06}, []byte{startFrame, 0x04, 0x01, 0x00, 0x0c, 0x00}}, 106 | } 107 | for _, tt := range tests { 108 | t.Run(tt.name, func(t *testing.T) { 109 | if got := newSFrame(tt.args.RcvSN); !reflect.DeepEqual(got, tt.want) { 110 | t.Errorf("newSFrame() = % x, want % x", got, tt.want) 111 | } 112 | }) 113 | } 114 | } 115 | 116 | func Test_newUFrame(t *testing.T) { 117 | type args struct { 118 | which byte 119 | } 120 | tests := []struct { 121 | name string 122 | args args 123 | want []byte 124 | }{ 125 | {"", args{uStopDtActive}, []byte{startFrame, 0x04, 0x13, 0x00, 0x00, 0x00}}, 126 | } 127 | for _, tt := range tests { 128 | t.Run(tt.name, func(t *testing.T) { 129 | if got := newUFrame(tt.args.which); !reflect.DeepEqual(got, tt.want) { 130 | t.Errorf("newUFrame() = % x, want % x", got, tt.want) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func Test_parse(t *testing.T) { 137 | type args struct { 138 | apdu []byte 139 | } 140 | tests := []struct { 141 | name string 142 | args args 143 | want interface{} 144 | want1 []byte 145 | }{ 146 | { 147 | "iAPCI", 148 | args{[]byte{startFrame, 0x04, 0x02, 0x00, 0x03, 0x00}}, 149 | iAPCI{sendSN: 0x01, rcvSN: 0x01}, 150 | []byte{}, 151 | }, 152 | { 153 | "sAPCI", 154 | args{[]byte{startFrame, 0x04, 0x01, 0x00, 0x02, 0x00}}, 155 | sAPCI{rcvSN: 0x01}, 156 | []byte{}, 157 | }, 158 | { 159 | "uAPCI", 160 | args{[]byte{startFrame, 0x04, 0x07, 0x00, 0x00, 0x00}}, 161 | uAPCI{uStartDtActive}, 162 | []byte{}, 163 | }, 164 | } 165 | for _, tt := range tests { 166 | t.Run(tt.name, func(t *testing.T) { 167 | got, got1 := parse(tt.args.apdu) 168 | if !reflect.DeepEqual(got, tt.want) { 169 | t.Errorf("parse() got = %v, want %v", got, tt.want) 170 | } 171 | if !reflect.DeepEqual(got1, tt.want1) { 172 | t.Errorf("parse() got1 = %v, want %v", got1, tt.want1) 173 | } 174 | }) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /cs104/client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "io" 11 | "math/rand" 12 | "net" 13 | "strings" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | 18 | "github.com/thinkgos/go-iecp5/asdu" 19 | "github.com/thinkgos/go-iecp5/clog" 20 | ) 21 | 22 | const ( 23 | inactive = iota 24 | active 25 | ) 26 | 27 | // Client is an IEC104 master 28 | type Client struct { 29 | option ClientOption 30 | conn net.Conn 31 | handler ClientHandlerInterface 32 | 33 | // channel 34 | rcvASDU chan []byte // for received asdu 35 | sendASDU chan []byte // for send asdu 36 | rcvRaw chan []byte // for recvLoop raw cs104 frame 37 | sendRaw chan []byte // for sendLoop raw cs104 frame 38 | 39 | // I帧的发送与接收序号 40 | seqNoSend uint16 // sequence number of next outbound I-frame 41 | ackNoSend uint16 // outbound sequence number yet to be confirmed 42 | seqNoRcv uint16 // sequence number of next inbound I-frame 43 | ackNoRcv uint16 // inbound sequence number yet to be confirmed 44 | 45 | // maps sendTime I-frames to their respective sequence number 46 | pending []seqPending 47 | 48 | startDtActiveSendSince atomic.Value // 当发送startDtActive时,等待确认回复的超时间隔 49 | stopDtActiveSendSince atomic.Value // 当发起stopDtActive时,等待确认回复的超时 50 | 51 | // 连接状态 52 | status uint32 53 | rwMux sync.RWMutex 54 | isActive uint32 55 | 56 | // 其他 57 | clog.Clog 58 | 59 | wg sync.WaitGroup 60 | ctx context.Context 61 | cancel context.CancelFunc 62 | closeCancel context.CancelFunc 63 | 64 | onConnect func(c *Client) 65 | onConnectionLost func(c *Client) 66 | } 67 | 68 | // NewClient returns an IEC104 master,default config and default asdu.ParamsWide params 69 | func NewClient(handler ClientHandlerInterface, o *ClientOption) *Client { 70 | return &Client{ 71 | option: *o, 72 | handler: handler, 73 | rcvASDU: make(chan []byte, o.config.RecvUnAckLimitW<<4), 74 | sendASDU: make(chan []byte, o.config.SendUnAckLimitK<<4), 75 | rcvRaw: make(chan []byte, o.config.RecvUnAckLimitW<<5), 76 | sendRaw: make(chan []byte, o.config.SendUnAckLimitK<<5), // may not block! 77 | Clog: clog.NewLogger("cs104 client => "), 78 | onConnect: func(*Client) {}, 79 | onConnectionLost: func(*Client) {}, 80 | } 81 | } 82 | 83 | // SetOnConnectHandler set on connect handler 84 | func (sf *Client) SetOnConnectHandler(f func(c *Client)) *Client { 85 | if f != nil { 86 | sf.onConnect = f 87 | } 88 | return sf 89 | } 90 | 91 | // SetConnectionLostHandler set connection lost handler 92 | func (sf *Client) SetConnectionLostHandler(f func(c *Client)) *Client { 93 | if f != nil { 94 | sf.onConnectionLost = f 95 | } 96 | return sf 97 | } 98 | 99 | // Start start the server,and return quickly,if it nil,the server will disconnected background,other failed 100 | func (sf *Client) Start() error { 101 | if sf.option.server == nil { 102 | return errors.New("empty remote server") 103 | } 104 | 105 | go sf.running() 106 | return nil 107 | } 108 | 109 | // Connect is 110 | func (sf *Client) running() { 111 | var ctx context.Context 112 | 113 | sf.rwMux.Lock() 114 | if !atomic.CompareAndSwapUint32(&sf.status, initial, disconnected) { 115 | sf.rwMux.Unlock() 116 | return 117 | } 118 | ctx, sf.closeCancel = context.WithCancel(context.Background()) 119 | sf.rwMux.Unlock() 120 | defer sf.setConnectStatus(initial) 121 | 122 | for { 123 | select { 124 | case <-ctx.Done(): 125 | return 126 | default: 127 | } 128 | 129 | sf.Debug("connecting server %+v", sf.option.server) 130 | conn, err := openConnection(sf.option.server, sf.option.TLSConfig, sf.option.config.ConnectTimeout0) 131 | if err != nil { 132 | sf.Error("connect failed, %v", err) 133 | if !sf.option.autoReconnect { 134 | return 135 | } 136 | time.Sleep(sf.option.reconnectInterval) 137 | continue 138 | } 139 | sf.Debug("connect success") 140 | sf.conn = conn 141 | sf.run(ctx) 142 | 143 | sf.Debug("disconnected server %+v", sf.option.server) 144 | select { 145 | case <-ctx.Done(): 146 | return 147 | default: 148 | // 随机500ms-1s的重试,避免快速重试造成服务器许多无效连接 149 | time.Sleep(time.Millisecond * time.Duration(500+rand.Intn(500))) 150 | } 151 | } 152 | } 153 | 154 | func (sf *Client) recvLoop() { 155 | sf.Debug("recvLoop started") 156 | defer func() { 157 | sf.cancel() 158 | sf.wg.Done() 159 | sf.Debug("recvLoop stopped") 160 | }() 161 | 162 | for { 163 | rawData := make([]byte, APDUSizeMax) 164 | for rdCnt, length := 0, 2; rdCnt < length; { 165 | byteCount, err := io.ReadFull(sf.conn, rawData[rdCnt:length]) 166 | if err != nil { 167 | // See: https://github.com/golang/go/issues/4373 168 | if err != io.EOF && err != io.ErrClosedPipe || 169 | strings.Contains(err.Error(), "use of closed network connection") { 170 | sf.Error("receive failed, %v", err) 171 | return 172 | } 173 | if e, ok := err.(net.Error); ok && !e.Temporary() { 174 | sf.Error("receive failed, %v", err) 175 | return 176 | } 177 | if rdCnt == 0 && err == io.EOF { 178 | sf.Error("remote connect closed, %v", err) 179 | return 180 | } 181 | } 182 | 183 | rdCnt += byteCount 184 | if rdCnt == 0 { 185 | continue 186 | } else if rdCnt == 1 { 187 | if rawData[0] != startFrame { 188 | rdCnt = 0 189 | continue 190 | } 191 | } else { 192 | if rawData[0] != startFrame { 193 | rdCnt, length = 0, 2 194 | continue 195 | } 196 | length = int(rawData[1]) + 2 197 | if length < APCICtlFiledSize+2 || length > APDUSizeMax { 198 | rdCnt, length = 0, 2 199 | continue 200 | } 201 | if rdCnt == length { 202 | apdu := rawData[:length] 203 | sf.Debug("RX Raw[% x]", apdu) 204 | sf.rcvRaw <- apdu 205 | } 206 | } 207 | } 208 | } 209 | } 210 | 211 | func (sf *Client) sendLoop() { 212 | sf.Debug("sendLoop started") 213 | defer func() { 214 | sf.cancel() 215 | sf.wg.Done() 216 | sf.Debug("sendLoop stopped") 217 | }() 218 | for { 219 | select { 220 | case <-sf.ctx.Done(): 221 | return 222 | case apdu := <-sf.sendRaw: 223 | sf.Debug("TX Raw[% x]", apdu) 224 | for wrCnt := 0; len(apdu) > wrCnt; { 225 | byteCount, err := sf.conn.Write(apdu[wrCnt:]) 226 | if err != nil { 227 | // See: https://github.com/golang/go/issues/4373 228 | if err != io.EOF && err != io.ErrClosedPipe || 229 | strings.Contains(err.Error(), "use of closed network connection") { 230 | sf.Error("sendRaw failed, %v", err) 231 | return 232 | } 233 | if e, ok := err.(net.Error); !ok || !e.Temporary() { 234 | sf.Error("sendRaw failed, %v", err) 235 | return 236 | } 237 | // temporary error may be recoverable 238 | } 239 | wrCnt += byteCount 240 | } 241 | } 242 | } 243 | } 244 | 245 | // run is the big fat state machine. 246 | func (sf *Client) run(ctx context.Context) { 247 | sf.Debug("run started!") 248 | // before any thing make sure init 249 | sf.cleanUp() 250 | 251 | sf.ctx, sf.cancel = context.WithCancel(ctx) 252 | sf.setConnectStatus(connected) 253 | sf.wg.Add(3) 254 | go sf.recvLoop() 255 | go sf.sendLoop() 256 | go sf.handlerLoop() 257 | 258 | var checkTicker = time.NewTicker(timeoutResolution) 259 | 260 | // transmission timestamps for timeout calculation 261 | var willNotTimeout = time.Now().Add(time.Hour * 24 * 365 * 100) 262 | 263 | var unAckRcvSince = willNotTimeout 264 | var idleTimeout3Sine = time.Now() // 空闲间隔发起testFrAlive 265 | var testFrAliveSendSince = willNotTimeout // 当发起testFrAlive时,等待确认回复的超时间隔 266 | 267 | sf.startDtActiveSendSince.Store(willNotTimeout) 268 | sf.stopDtActiveSendSince.Store(willNotTimeout) 269 | 270 | sendSFrame := func(rcvSN uint16) { 271 | sf.Debug("TX sFrame %v", sAPCI{rcvSN}) 272 | sf.sendRaw <- newSFrame(rcvSN) 273 | } 274 | 275 | sendIFrame := func(asdu1 []byte) { 276 | seqNo := sf.seqNoSend 277 | 278 | iframe, err := newIFrame(seqNo, sf.seqNoRcv, asdu1) 279 | if err != nil { 280 | return 281 | } 282 | sf.ackNoRcv = sf.seqNoRcv 283 | sf.seqNoSend = (seqNo + 1) & 32767 284 | sf.pending = append(sf.pending, seqPending{seqNo & 32767, time.Now()}) 285 | 286 | sf.Debug("TX iFrame %v", iAPCI{seqNo, sf.seqNoRcv}) 287 | sf.sendRaw <- iframe 288 | } 289 | 290 | defer func() { 291 | // default: STOPDT, when connected establish and not enable "data transfer" yet 292 | atomic.StoreUint32(&sf.isActive, inactive) 293 | sf.setConnectStatus(disconnected) 294 | checkTicker.Stop() 295 | _ = sf.conn.Close() // 连锁引发cancel 296 | sf.wg.Wait() 297 | sf.onConnectionLost(sf) 298 | sf.Debug("run stopped!") 299 | }() 300 | 301 | sf.onConnect(sf) 302 | for { 303 | if atomic.LoadUint32(&sf.isActive) == active && seqNoCount(sf.ackNoSend, sf.seqNoSend) <= sf.option.config.SendUnAckLimitK { 304 | select { 305 | case o := <-sf.sendASDU: 306 | sendIFrame(o) 307 | idleTimeout3Sine = time.Now() 308 | continue 309 | case <-sf.ctx.Done(): 310 | return 311 | default: // make no block 312 | } 313 | } 314 | select { 315 | case <-sf.ctx.Done(): 316 | return 317 | case now := <-checkTicker.C: 318 | // check all timeouts 319 | if now.Sub(testFrAliveSendSince) >= sf.option.config.SendUnAckTimeout1 || 320 | now.Sub(sf.startDtActiveSendSince.Load().(time.Time)) >= sf.option.config.SendUnAckTimeout1 || 321 | now.Sub(sf.stopDtActiveSendSince.Load().(time.Time)) >= sf.option.config.SendUnAckTimeout1 { 322 | sf.Error("test frame alive confirm timeout t₁") 323 | return 324 | } 325 | // check oldest unacknowledged outbound 326 | if sf.ackNoSend != sf.seqNoSend && 327 | //now.Sub(sf.peek()) >= sf.SendUnAckTimeout1 { 328 | now.Sub(sf.pending[0].sendTime) >= sf.option.config.SendUnAckTimeout1 { 329 | sf.ackNoSend++ 330 | sf.Error("fatal transmission timeout t₁") 331 | return 332 | } 333 | 334 | // 确定最早发送的i-Frame是否超时,超时则回复sFrame 335 | if sf.ackNoRcv != sf.seqNoRcv && 336 | (now.Sub(unAckRcvSince) >= sf.option.config.RecvUnAckTimeout2 || 337 | now.Sub(idleTimeout3Sine) >= timeoutResolution) { 338 | sendSFrame(sf.seqNoRcv) 339 | sf.ackNoRcv = sf.seqNoRcv 340 | } 341 | 342 | // 空闲时间到,发送TestFrActive帧,保活 343 | if now.Sub(idleTimeout3Sine) >= sf.option.config.IdleTimeout3 { 344 | sf.sendUFrame(uTestFrActive) 345 | testFrAliveSendSince = time.Now() 346 | idleTimeout3Sine = testFrAliveSendSince 347 | } 348 | 349 | case apdu := <-sf.rcvRaw: 350 | idleTimeout3Sine = time.Now() // 每收到一个i帧,S帧,U帧, 重置空闲定时器, t3 351 | apci, asduVal := parse(apdu) 352 | switch head := apci.(type) { 353 | case sAPCI: 354 | sf.Debug("RX sFrame %v", head) 355 | if !sf.updateAckNoOut(head.rcvSN) { 356 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 357 | return 358 | } 359 | 360 | case iAPCI: 361 | sf.Debug("RX iFrame %v", head) 362 | if atomic.LoadUint32(&sf.isActive) == inactive { 363 | sf.Warn("station not active") 364 | break // not active, discard apdu 365 | } 366 | if !sf.updateAckNoOut(head.rcvSN) || head.sendSN != sf.seqNoRcv { 367 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 368 | return 369 | } 370 | 371 | sf.rcvASDU <- asduVal 372 | if sf.ackNoRcv == sf.seqNoRcv { // first unacked 373 | unAckRcvSince = time.Now() 374 | } 375 | 376 | sf.seqNoRcv = (sf.seqNoRcv + 1) & 32767 377 | if seqNoCount(sf.ackNoRcv, sf.seqNoRcv) >= sf.option.config.RecvUnAckLimitW { 378 | sendSFrame(sf.seqNoRcv) 379 | sf.ackNoRcv = sf.seqNoRcv 380 | } 381 | 382 | case uAPCI: 383 | sf.Debug("RX uFrame %v", head) 384 | switch head.function { 385 | //case uStartDtActive: 386 | // sf.sendUFrame(uStartDtConfirm) 387 | // atomic.StoreUint32(&sf.isActive, active) 388 | case uStartDtConfirm: 389 | atomic.StoreUint32(&sf.isActive, active) 390 | sf.startDtActiveSendSince.Store(willNotTimeout) 391 | //case uStopDtActive: 392 | // sf.sendUFrame(uStopDtConfirm) 393 | // atomic.StoreUint32(&sf.isActive, inactive) 394 | case uStopDtConfirm: 395 | atomic.StoreUint32(&sf.isActive, inactive) 396 | sf.stopDtActiveSendSince.Store(willNotTimeout) 397 | case uTestFrActive: 398 | sf.sendUFrame(uTestFrConfirm) 399 | case uTestFrConfirm: 400 | testFrAliveSendSince = willNotTimeout 401 | default: 402 | sf.Error("illegal U-Frame functions[0x%02x] ignored", head.function) 403 | } 404 | } 405 | } 406 | } 407 | } 408 | 409 | func (sf *Client) handlerLoop() { 410 | sf.Debug("handlerLoop started") 411 | defer func() { 412 | sf.wg.Done() 413 | sf.Debug("handlerLoop stopped") 414 | }() 415 | 416 | for { 417 | select { 418 | case <-sf.ctx.Done(): 419 | return 420 | case rawAsdu := <-sf.rcvASDU: 421 | asduPack := asdu.NewEmptyASDU(&sf.option.params) 422 | if err := asduPack.UnmarshalBinary(rawAsdu); err != nil { 423 | sf.Warn("asdu UnmarshalBinary failed,%+v", err) 424 | continue 425 | } 426 | if err := sf.clientHandler(asduPack); err != nil { 427 | sf.Warn("Falied handling I frame, error: %v", err) 428 | } 429 | } 430 | } 431 | } 432 | 433 | func (sf *Client) setConnectStatus(status uint32) { 434 | sf.rwMux.Lock() 435 | atomic.StoreUint32(&sf.status, status) 436 | sf.rwMux.Unlock() 437 | } 438 | 439 | func (sf *Client) connectStatus() uint32 { 440 | sf.rwMux.RLock() 441 | status := atomic.LoadUint32(&sf.status) 442 | sf.rwMux.RUnlock() 443 | return status 444 | } 445 | 446 | func (sf *Client) cleanUp() { 447 | sf.ackNoRcv = 0 448 | sf.ackNoSend = 0 449 | sf.seqNoRcv = 0 450 | sf.seqNoSend = 0 451 | sf.pending = nil 452 | // clear sending chan buffer 453 | loop: 454 | for { 455 | select { 456 | case <-sf.sendRaw: 457 | case <-sf.rcvRaw: 458 | case <-sf.rcvASDU: 459 | case <-sf.sendASDU: 460 | default: 461 | break loop 462 | } 463 | } 464 | } 465 | 466 | func (sf *Client) sendUFrame(which byte) { 467 | sf.Debug("TX uFrame %v", uAPCI{which}) 468 | sf.sendRaw <- newUFrame(which) 469 | } 470 | 471 | func (sf *Client) updateAckNoOut(ackNo uint16) (ok bool) { 472 | if ackNo == sf.ackNoSend { 473 | return true 474 | } 475 | // new acks validate, ack 不能在 req seq 前面,出错 476 | if seqNoCount(sf.ackNoSend, sf.seqNoSend) < seqNoCount(ackNo, sf.seqNoSend) { 477 | return false 478 | } 479 | 480 | // confirm reception 481 | for i, v := range sf.pending { 482 | if v.seq == (ackNo - 1) { 483 | sf.pending = sf.pending[i+1:] 484 | break 485 | } 486 | } 487 | 488 | sf.ackNoSend = ackNo 489 | return true 490 | } 491 | 492 | // IsConnected get server session connected state 493 | func (sf *Client) IsConnected() bool { 494 | return sf.connectStatus() == connected 495 | } 496 | 497 | // clientHandler hand response handler 498 | func (sf *Client) clientHandler(asduPack *asdu.ASDU) error { 499 | defer func() { 500 | if err := recover(); err != nil { 501 | sf.Critical("client handler %+v", err) 502 | } 503 | }() 504 | 505 | sf.Debug("ASDU %+v", asduPack) 506 | 507 | switch asduPack.Identifier.Type { 508 | case asdu.C_IC_NA_1: // InterrogationCmd 509 | return sf.handler.InterrogationHandler(sf, asduPack) 510 | 511 | case asdu.C_CI_NA_1: // CounterInterrogationCmd 512 | return sf.handler.CounterInterrogationHandler(sf, asduPack) 513 | 514 | case asdu.C_RD_NA_1: // ReadCmd 515 | return sf.handler.ReadHandler(sf, asduPack) 516 | 517 | case asdu.C_CS_NA_1: // ClockSynchronizationCmd 518 | return sf.handler.ClockSyncHandler(sf, asduPack) 519 | 520 | case asdu.C_TS_NA_1: // TestCommand 521 | return sf.handler.TestCommandHandler(sf, asduPack) 522 | 523 | case asdu.C_RP_NA_1: // ResetProcessCmd 524 | return sf.handler.ResetProcessHandler(sf, asduPack) 525 | 526 | case asdu.C_CD_NA_1: // DelayAcquireCommand 527 | return sf.handler.DelayAcquisitionHandler(sf, asduPack) 528 | } 529 | 530 | return sf.handler.ASDUHandler(sf, asduPack) 531 | } 532 | 533 | // Params returns params of client 534 | func (sf *Client) Params() *asdu.Params { 535 | return &sf.option.params 536 | } 537 | 538 | // Send send asdu 539 | func (sf *Client) Send(a *asdu.ASDU) error { 540 | if !sf.IsConnected() { 541 | return ErrUseClosedConnection 542 | } 543 | if atomic.LoadUint32(&sf.isActive) == inactive { 544 | return ErrNotActive 545 | } 546 | data, err := a.MarshalBinary() 547 | if err != nil { 548 | return err 549 | } 550 | select { 551 | case sf.sendASDU <- data: 552 | default: 553 | return ErrBufferFulled 554 | } 555 | return nil 556 | } 557 | 558 | // UnderlyingConn returns underlying conn of client 559 | func (sf *Client) UnderlyingConn() net.Conn { 560 | return sf.conn 561 | } 562 | 563 | // Close close all 564 | func (sf *Client) Close() error { 565 | sf.rwMux.Lock() 566 | if sf.closeCancel != nil { 567 | sf.closeCancel() 568 | } 569 | sf.rwMux.Unlock() 570 | return nil 571 | } 572 | 573 | // SendStartDt start data transmission on this connection 574 | func (sf *Client) SendStartDt() { 575 | sf.startDtActiveSendSince.Store(time.Now()) 576 | sf.sendUFrame(uStartDtActive) 577 | } 578 | 579 | // SendStopDt stop data transmission on this connection 580 | func (sf *Client) SendStopDt() { 581 | sf.stopDtActiveSendSince.Store(time.Now()) 582 | sf.sendUFrame(uStopDtActive) 583 | } 584 | 585 | //InterrogationCmd wrap asdu.InterrogationCmd 586 | func (sf *Client) InterrogationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qoi asdu.QualifierOfInterrogation) error { 587 | return asdu.InterrogationCmd(sf, coa, ca, qoi) 588 | } 589 | 590 | // CounterInterrogationCmd wrap asdu.CounterInterrogationCmd 591 | func (sf *Client) CounterInterrogationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qcc asdu.QualifierCountCall) error { 592 | return asdu.CounterInterrogationCmd(sf, coa, ca, qcc) 593 | } 594 | 595 | // ReadCmd wrap asdu.ReadCmd 596 | func (sf *Client) ReadCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, ioa asdu.InfoObjAddr) error { 597 | return asdu.ReadCmd(sf, coa, ca, ioa) 598 | } 599 | 600 | // ClockSynchronizationCmd wrap asdu.ClockSynchronizationCmd 601 | func (sf *Client) ClockSynchronizationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, t time.Time) error { 602 | return asdu.ClockSynchronizationCmd(sf, coa, ca, t) 603 | } 604 | 605 | // ResetProcessCmd wrap asdu.ResetProcessCmd 606 | func (sf *Client) ResetProcessCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qrp asdu.QualifierOfResetProcessCmd) error { 607 | return asdu.ResetProcessCmd(sf, coa, ca, qrp) 608 | } 609 | 610 | // DelayAcquireCommand wrap asdu.DelayAcquireCommand 611 | func (sf *Client) DelayAcquireCommand(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, msec uint16) error { 612 | return asdu.DelayAcquireCommand(sf, coa, ca, msec) 613 | } 614 | 615 | // TestCommand wrap asdu.TestCommand 616 | func (sf *Client) TestCommand(coa asdu.CauseOfTransmission, ca asdu.CommonAddr) error { 617 | return asdu.TestCommand(sf, coa, ca) 618 | } 619 | -------------------------------------------------------------------------------- /cs104/clientOption.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "crypto/tls" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/thinkgos/go-iecp5/asdu" 14 | ) 15 | 16 | // ClientOption 客户端配置 17 | type ClientOption struct { 18 | config Config 19 | params asdu.Params 20 | server *url.URL // 连接的服务器端 21 | autoReconnect bool // 是否启动重连 22 | reconnectInterval time.Duration // 重连间隔时间 23 | TLSConfig *tls.Config // tls配置 24 | } 25 | 26 | // NewOption with default config and default asdu.ParamsWide params 27 | func NewOption() *ClientOption { 28 | return &ClientOption{ 29 | DefaultConfig(), 30 | *asdu.ParamsWide, 31 | nil, 32 | true, 33 | DefaultReconnectInterval, 34 | nil, 35 | } 36 | } 37 | 38 | // SetConfig set config if config is valid it will use DefaultConfig() 39 | func (sf *ClientOption) SetConfig(cfg Config) *ClientOption { 40 | if err := cfg.Valid(); err != nil { 41 | sf.config = DefaultConfig() 42 | } else { 43 | sf.config = cfg 44 | } 45 | return sf 46 | } 47 | 48 | // SetParams set asdu params if params is valid it will use asdu.ParamsWide 49 | func (sf *ClientOption) SetParams(p *asdu.Params) *ClientOption { 50 | if err := p.Valid(); err != nil { 51 | sf.params = *asdu.ParamsWide 52 | } else { 53 | sf.params = *p 54 | } 55 | return sf 56 | } 57 | 58 | // SetReconnectInterval set tcp reconnect the host interval when connect failed after try 59 | func (sf *ClientOption) SetReconnectInterval(t time.Duration) *ClientOption { 60 | if t > 0 { 61 | sf.reconnectInterval = t 62 | } 63 | return sf 64 | } 65 | 66 | // SetAutoReconnect enable auto reconnect 67 | func (sf *ClientOption) SetAutoReconnect(b bool) *ClientOption { 68 | sf.autoReconnect = b 69 | return sf 70 | } 71 | 72 | // SetTLSConfig set tls config 73 | func (sf *ClientOption) SetTLSConfig(t *tls.Config) *ClientOption { 74 | sf.TLSConfig = t 75 | return sf 76 | } 77 | 78 | // AddRemoteServer adds a broker URI to the list of brokers to be used. 79 | // The format should be scheme://host:port 80 | // Default values for hostname is "127.0.0.1", for schema is "tcp://". 81 | // An example broker URI would look like: tcp://foobar.com:1204 82 | func (sf *ClientOption) AddRemoteServer(server string) error { 83 | if len(server) > 0 && server[0] == ':' { 84 | server = "127.0.0.1" + server 85 | } 86 | if !strings.Contains(server, "://") { 87 | server = "tcp://" + server 88 | } 89 | remoteURL, err := url.Parse(server) 90 | if err != nil { 91 | return err 92 | } 93 | sf.server = remoteURL 94 | return nil 95 | } 96 | -------------------------------------------------------------------------------- /cs104/common.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "crypto/tls" 9 | "errors" 10 | "net" 11 | "net/url" 12 | "time" 13 | ) 14 | 15 | // DefaultReconnectInterval defined default value 16 | const DefaultReconnectInterval = 1 * time.Minute 17 | 18 | type seqPending struct { 19 | seq uint16 20 | sendTime time.Time 21 | } 22 | 23 | func openConnection(uri *url.URL, tlsc *tls.Config, timeout time.Duration) (net.Conn, error) { 24 | switch uri.Scheme { 25 | case "tcp": 26 | return net.DialTimeout("tcp", uri.Host, timeout) 27 | case "ssl": 28 | fallthrough 29 | case "tls": 30 | fallthrough 31 | case "tcps": 32 | return tls.DialWithDialer(&net.Dialer{Timeout: timeout}, "tcp", uri.Host, tlsc) 33 | } 34 | return nil, errors.New("Unknown protocol") 35 | } 36 | -------------------------------------------------------------------------------- /cs104/config.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "errors" 9 | "time" 10 | ) 11 | 12 | const ( 13 | // Port is the IANA registered port number for unsecure connection. 14 | Port = 2404 15 | 16 | // PortSecure is the IANA registered port number for secure connection. 17 | PortSecure = 19998 18 | ) 19 | 20 | // defines an IEC 60870-5-104 configuration range 21 | const ( 22 | // "t₀" 范围[1, 255]s 默认 30s 23 | ConnectTimeout0Min = 1 * time.Second 24 | ConnectTimeout0Max = 255 * time.Second 25 | 26 | // "t₁" 范围[1, 255]s 默认 15s. See IEC 60870-5-104, figure 18. 27 | SendUnAckTimeout1Min = 1 * time.Second 28 | SendUnAckTimeout1Max = 255 * time.Second 29 | 30 | // "t₂" 范围[1, 255]s 默认 10s, See IEC 60870-5-104, figure 10. 31 | RecvUnAckTimeout2Min = 1 * time.Second 32 | RecvUnAckTimeout2Max = 255 * time.Second 33 | 34 | // "t₃" 范围[1 second, 48 hours] 默认 20 s, See IEC 60870-5-104, subclass 5.2. 35 | IdleTimeout3Min = 1 * time.Second 36 | IdleTimeout3Max = 48 * time.Hour 37 | 38 | // "k" 范围[1, 32767] 默认 12. See IEC 60870-5-104, subclass 5.5. 39 | SendUnAckLimitKMin = 1 40 | SendUnAckLimitKMax = 32767 41 | 42 | // "w" 范围 [1, 32767] 默认 8. See IEC 60870-5-104, subclass 5.5. 43 | RecvUnAckLimitWMin = 1 44 | RecvUnAckLimitWMax = 32767 45 | ) 46 | 47 | // Config defines an IEC 60870-5-104 configuration. 48 | // The default is applied for each unspecified value. 49 | type Config struct { 50 | // tcp连接建立的最大超时时间 51 | // "t₀" 范围[1, 255]s,默认 30s. 52 | ConnectTimeout0 time.Duration 53 | 54 | // I-frames 发送未收到确认的帧数上限, 一旦达到这个数,将停止传输 55 | // "k" 范围[1, 32767] 默认 12. 56 | // See IEC 60870-5-104, subclass 5.5. 57 | SendUnAckLimitK uint16 58 | 59 | // 帧接收确认最长超时时间,超过此时间立即关闭连接。 60 | // "t₁" 范围[1, 255]s 默认 15s. 61 | // See IEC 60870-5-104, figure 18. 62 | SendUnAckTimeout1 time.Duration 63 | 64 | // 接收端最迟在接收了w次I-frames应用规约数据单元以后发出认可。 w不超过2/3k(2/3 SendUnAckLimitK) 65 | // "w" 范围 [1, 32767] 默认 8. 66 | // See IEC 60870-5-104, subclass 5.5. 67 | RecvUnAckLimitW uint16 68 | 69 | // 发送一个接收确认的最大时间,实际上这个框架1秒内发送回复 70 | // "t₂" 范围[1, 255]s 默认 10s 71 | // See IEC 60870-5-104, figure 10. 72 | RecvUnAckTimeout2 time.Duration 73 | 74 | // 触发 "TESTFR" 保活的空闲时间值, 75 | // "t₃" 范围[1 second, 48 hours] 默认 20 s 76 | // See IEC 60870-5-104, subclass 5.2. 77 | IdleTimeout3 time.Duration 78 | } 79 | 80 | // Valid applies the default (defined by IEC) for each unspecified value. 81 | func (sf *Config) Valid() error { 82 | if sf == nil { 83 | return errors.New("invalid pointer") 84 | } 85 | 86 | if sf.ConnectTimeout0 == 0 { 87 | sf.ConnectTimeout0 = 30 * time.Second 88 | } else if sf.ConnectTimeout0 < ConnectTimeout0Min || sf.ConnectTimeout0 > ConnectTimeout0Max { 89 | return errors.New(`ConnectTimeout0 "t₀" not in [1, 255]s`) 90 | } 91 | 92 | if sf.SendUnAckLimitK == 0 { 93 | sf.SendUnAckLimitK = 12 94 | } else if sf.SendUnAckLimitK < SendUnAckLimitKMin || sf.SendUnAckLimitK > SendUnAckLimitKMax { 95 | return errors.New(`SendUnAckLimitK "k" not in [1, 32767]`) 96 | } 97 | 98 | if sf.SendUnAckTimeout1 == 0 { 99 | sf.SendUnAckTimeout1 = 15 * time.Second 100 | } else if sf.SendUnAckTimeout1 < SendUnAckTimeout1Min || sf.SendUnAckTimeout1 > SendUnAckTimeout1Max { 101 | return errors.New(`SendUnAckTimeout1 "t₁" not in [1, 255]s`) 102 | } 103 | 104 | if sf.RecvUnAckLimitW == 0 { 105 | sf.RecvUnAckLimitW = 8 106 | } else if sf.RecvUnAckLimitW < RecvUnAckLimitWMin || sf.RecvUnAckLimitW > RecvUnAckLimitWMax { 107 | return errors.New(`RecvUnAckLimitW "w" not in [1, 32767]`) 108 | } 109 | 110 | if sf.RecvUnAckTimeout2 == 0 { 111 | sf.RecvUnAckTimeout2 = 10 * time.Second 112 | } else if sf.RecvUnAckTimeout2 < RecvUnAckTimeout2Min || sf.RecvUnAckTimeout2 > RecvUnAckTimeout2Max { 113 | return errors.New(`RecvUnAckTimeout2 "t₂" not in [1, 255]s`) 114 | } 115 | 116 | if sf.IdleTimeout3 == 0 { 117 | sf.IdleTimeout3 = 20 * time.Second 118 | } else if sf.IdleTimeout3 < IdleTimeout3Min || sf.IdleTimeout3 > IdleTimeout3Max { 119 | return errors.New(`IdleTimeout3 "t₃" not in [1 second, 48 hours]`) 120 | } 121 | 122 | return nil 123 | } 124 | 125 | // DefaultConfig default config 126 | func DefaultConfig() Config { 127 | return Config{ 128 | 30 * time.Second, 129 | 12, 130 | 15 * time.Second, 131 | 8, 132 | 10 * time.Second, 133 | 20 * time.Second, 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /cs104/error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "errors" 9 | ) 10 | 11 | // error defined 12 | var ( 13 | ErrUseClosedConnection = errors.New("use of closed connection") 14 | ErrBufferFulled = errors.New("buffer is full") 15 | ErrNotActive = errors.New("server is not active") 16 | ) 17 | -------------------------------------------------------------------------------- /cs104/interface.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/thinkgos/go-iecp5/asdu" 11 | ) 12 | 13 | // ServerHandlerInterface is the interface of server handler 14 | type ServerHandlerInterface interface { 15 | InterrogationHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierOfInterrogation) error 16 | CounterInterrogationHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierCountCall) error 17 | ReadHandler(asdu.Connect, *asdu.ASDU, asdu.InfoObjAddr) error 18 | ClockSyncHandler(asdu.Connect, *asdu.ASDU, time.Time) error 19 | ResetProcessHandler(asdu.Connect, *asdu.ASDU, asdu.QualifierOfResetProcessCmd) error 20 | DelayAcquisitionHandler(asdu.Connect, *asdu.ASDU, uint16) error 21 | ASDUHandler(asdu.Connect, *asdu.ASDU) error 22 | } 23 | 24 | // ClientHandlerInterface is the interface of client handler 25 | type ClientHandlerInterface interface { 26 | InterrogationHandler(asdu.Connect, *asdu.ASDU) error 27 | CounterInterrogationHandler(asdu.Connect, *asdu.ASDU) error 28 | ReadHandler(asdu.Connect, *asdu.ASDU) error 29 | TestCommandHandler(asdu.Connect, *asdu.ASDU) error 30 | ClockSyncHandler(asdu.Connect, *asdu.ASDU) error 31 | ResetProcessHandler(asdu.Connect, *asdu.ASDU) error 32 | DelayAcquisitionHandler(asdu.Connect, *asdu.ASDU) error 33 | ASDUHandler(asdu.Connect, *asdu.ASDU) error 34 | } 35 | -------------------------------------------------------------------------------- /cs104/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "context" 9 | "crypto/tls" 10 | "net" 11 | "sync" 12 | "time" 13 | 14 | "github.com/thinkgos/go-iecp5/asdu" 15 | "github.com/thinkgos/go-iecp5/clog" 16 | ) 17 | 18 | // timeoutResolution is seconds according to companion standard 104, 19 | // subclass 6.9, caption "Definition of time outs". However, then 20 | // of a second make this system much more responsive i.c.w. S-frames. 21 | const timeoutResolution = 100 * time.Millisecond 22 | 23 | // Server the common server 24 | type Server struct { 25 | config Config 26 | params asdu.Params 27 | handler ServerHandlerInterface 28 | TLSConfig *tls.Config 29 | mux sync.Mutex 30 | sessions map[*SrvSession]struct{} 31 | listen net.Listener 32 | onConnection func(asdu.Connect) 33 | connectionLost func(asdu.Connect) 34 | clog.Clog 35 | wg sync.WaitGroup 36 | } 37 | 38 | // NewServer new a server, default config and default asdu.ParamsWide params 39 | func NewServer(handler ServerHandlerInterface) *Server { 40 | return &Server{ 41 | config: DefaultConfig(), 42 | params: *asdu.ParamsWide, 43 | handler: handler, 44 | sessions: make(map[*SrvSession]struct{}), 45 | Clog: clog.NewLogger("cs104 server => "), 46 | } 47 | } 48 | 49 | // SetConfig set config if config is valid it will use DefaultConfig() 50 | func (sf *Server) SetConfig(cfg Config) *Server { 51 | if err := cfg.Valid(); err != nil { 52 | sf.config = DefaultConfig() 53 | } else { 54 | sf.config = cfg 55 | } 56 | return sf 57 | } 58 | 59 | // SetParams set asdu params if params is valid it will use asdu.ParamsWide 60 | func (sf *Server) SetParams(p *asdu.Params) *Server { 61 | if err := p.Valid(); err != nil { 62 | sf.params = *asdu.ParamsWide 63 | } else { 64 | sf.params = *p 65 | } 66 | return sf 67 | } 68 | 69 | // ListenAndServer run the server 70 | func (sf *Server) ListenAndServer(addr string) { 71 | listen, err := net.Listen("tcp", addr) 72 | if err != nil { 73 | sf.Error("server run failed, %v", err) 74 | return 75 | } 76 | sf.mux.Lock() 77 | sf.listen = listen 78 | sf.mux.Unlock() 79 | 80 | ctx, cancel := context.WithCancel(context.Background()) 81 | defer func() { 82 | cancel() 83 | _ = sf.Close() 84 | sf.Debug("server stop") 85 | }() 86 | sf.Debug("server run") 87 | for { 88 | conn, err := listen.Accept() 89 | if err != nil { 90 | sf.Error("server run failed, %v", err) 91 | return 92 | } 93 | 94 | sf.wg.Add(1) 95 | go func() { 96 | sess := &SrvSession{ 97 | config: &sf.config, 98 | params: &sf.params, 99 | handler: sf.handler, 100 | conn: conn, 101 | rcvASDU: make(chan []byte, sf.config.RecvUnAckLimitW<<4), 102 | sendASDU: make(chan []byte, sf.config.SendUnAckLimitK<<4), 103 | rcvRaw: make(chan []byte, sf.config.RecvUnAckLimitW<<5), 104 | sendRaw: make(chan []byte, sf.config.SendUnAckLimitK<<5), // may not block! 105 | 106 | onConnection: sf.onConnection, 107 | connectionLost: sf.connectionLost, 108 | Clog: sf.Clog, 109 | } 110 | sf.mux.Lock() 111 | sf.sessions[sess] = struct{}{} 112 | sf.mux.Unlock() 113 | sess.run(ctx) 114 | sf.mux.Lock() 115 | delete(sf.sessions, sess) 116 | sf.mux.Unlock() 117 | sf.wg.Done() 118 | }() 119 | } 120 | } 121 | 122 | // Close close the server 123 | func (sf *Server) Close() error { 124 | var err error 125 | 126 | sf.mux.Lock() 127 | if sf.listen != nil { 128 | err = sf.listen.Close() 129 | sf.listen = nil 130 | } 131 | sf.mux.Unlock() 132 | sf.wg.Wait() 133 | return err 134 | } 135 | 136 | // Send imp interface Connect 137 | func (sf *Server) Send(a *asdu.ASDU) error { 138 | sf.mux.Lock() 139 | for k := range sf.sessions { 140 | _ = k.Send(a.Clone()) 141 | } 142 | sf.mux.Unlock() 143 | return nil 144 | } 145 | 146 | // Params imp interface Connect 147 | func (sf *Server) Params() *asdu.Params { return &sf.params } 148 | 149 | // UnderlyingConn imp interface Connect 150 | func (sf *Server) UnderlyingConn() net.Conn { return nil } 151 | 152 | // SetInfoObjTimeZone set info object time zone 153 | func (sf *Server) SetInfoObjTimeZone(zone *time.Location) { 154 | sf.params.InfoObjTimeZone = zone 155 | } 156 | 157 | // SetOnConnectionHandler set on connect handler 158 | func (sf *Server) SetOnConnectionHandler(f func(asdu.Connect)) { 159 | sf.onConnection = f 160 | } 161 | 162 | // SetConnectionLostHandler set connect lost handler 163 | func (sf *Server) SetConnectionLostHandler(f func(asdu.Connect)) { 164 | sf.connectionLost = f 165 | } 166 | -------------------------------------------------------------------------------- /cs104/server_session.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "context" 9 | "io" 10 | "net" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/thinkgos/go-iecp5/asdu" 17 | "github.com/thinkgos/go-iecp5/clog" 18 | ) 19 | 20 | const ( 21 | initial uint32 = iota 22 | disconnected 23 | connected 24 | ) 25 | 26 | // SrvSession the cs104 server session 27 | type SrvSession struct { 28 | config *Config 29 | params *asdu.Params 30 | conn net.Conn 31 | handler ServerHandlerInterface 32 | 33 | rcvASDU chan []byte // for received asdu 34 | sendASDU chan []byte // for send asdu 35 | rcvRaw chan []byte // for recvLoop raw cs104 frame 36 | sendRaw chan []byte // for sendLoop raw cs104 frame 37 | 38 | // see subclass 5.1 — Protection against loss and duplication of messages 39 | seqNoSend uint16 // sequence number of next outbound I-frame 40 | ackNoSend uint16 // outbound sequence number yet to be confirmed 41 | seqNoRcv uint16 // sequence number of next inbound I-frame 42 | ackNoRcv uint16 // inbound sequence number yet to be confirmed 43 | // maps sendTime I-frames to their respective sequence number 44 | pending []seqPending 45 | //seqManage 46 | 47 | status uint32 48 | rwMux sync.RWMutex 49 | 50 | clog.Clog 51 | 52 | onConnection func(asdu.Connect) 53 | connectionLost func(asdu.Connect) 54 | 55 | wg sync.WaitGroup 56 | cancel context.CancelFunc 57 | ctx context.Context 58 | } 59 | 60 | // RecvLoop feeds t.rcvRaw. 61 | func (sf *SrvSession) recvLoop() { 62 | sf.Debug("recvLoop started!") 63 | defer func() { 64 | sf.cancel() 65 | sf.wg.Done() 66 | sf.Debug("recvLoop stopped!") 67 | }() 68 | 69 | for { 70 | rawData := make([]byte, APDUSizeMax) 71 | for rdCnt, length := 0, 2; rdCnt < length; { 72 | byteCount, err := io.ReadFull(sf.conn, rawData[rdCnt:length]) 73 | if err != nil { 74 | // See: https://github.com/golang/go/issues/4373 75 | if err != io.EOF && err != io.ErrClosedPipe || 76 | strings.Contains(err.Error(), "use of closed network connection") { 77 | sf.Error("receive failed, %v", err) 78 | return 79 | } 80 | 81 | if e, ok := err.(net.Error); ok && !e.Temporary() { 82 | sf.Error("receive failed, %v", err) 83 | return 84 | } 85 | 86 | if byteCount == 0 && err == io.EOF { 87 | sf.Error("remote connect closed, %v", err) 88 | return 89 | } 90 | } 91 | 92 | rdCnt += byteCount 93 | if rdCnt == 0 { 94 | continue 95 | } else if rdCnt == 1 { 96 | if rawData[0] != startFrame { 97 | rdCnt = 0 98 | continue 99 | } 100 | } else { 101 | if rawData[0] != startFrame { 102 | rdCnt, length = 0, 2 103 | continue 104 | } 105 | length = int(rawData[1]) + 2 106 | if length < APCICtlFiledSize+2 || length > APDUSizeMax { 107 | rdCnt, length = 0, 2 108 | continue 109 | } 110 | if rdCnt == length { 111 | apdu := rawData[:length] 112 | sf.Debug("RX Raw[% x]", apdu) 113 | sf.rcvRaw <- apdu 114 | } 115 | } 116 | } 117 | } 118 | } 119 | 120 | // sendLoop drains t.sendTime. 121 | func (sf *SrvSession) sendLoop() { 122 | sf.Debug("sendLoop started!") 123 | defer func() { 124 | sf.cancel() 125 | sf.wg.Done() 126 | sf.Debug("sendLoop stopped!") 127 | }() 128 | 129 | for { 130 | select { 131 | case <-sf.ctx.Done(): 132 | return 133 | case apdu := <-sf.sendRaw: 134 | sf.Debug("TX Raw[% x]", apdu) 135 | for wrCnt := 0; len(apdu) > wrCnt; { 136 | byteCount, err := sf.conn.Write(apdu[wrCnt:]) 137 | if err != nil { 138 | // See: https://github.com/golang/go/issues/4373 139 | if err != io.EOF && err != io.ErrClosedPipe || 140 | strings.Contains(err.Error(), "use of closed network connection") { 141 | sf.Error("sendRaw failed, %v", err) 142 | return 143 | } 144 | if e, ok := err.(net.Error); !ok || !e.Temporary() { 145 | sf.Error("sendRaw failed, %v", err) 146 | return 147 | } 148 | // temporary error may be recoverable 149 | } 150 | wrCnt += byteCount 151 | } 152 | } 153 | } 154 | } 155 | 156 | // run is the big fat state machine. 157 | func (sf *SrvSession) run(ctx context.Context) { 158 | sf.Debug("run started!") 159 | // before any thing make sure init 160 | sf.cleanUp() 161 | 162 | sf.ctx, sf.cancel = context.WithCancel(ctx) 163 | sf.setConnectStatus(connected) 164 | sf.wg.Add(3) 165 | go sf.recvLoop() 166 | go sf.sendLoop() 167 | go sf.handlerLoop() 168 | 169 | // default: STOPDT, when connected establish and not enable "data transfer" yet 170 | var isActive = false 171 | var checkTicker = time.NewTicker(timeoutResolution) 172 | 173 | // transmission timestamps for timeout calculation 174 | var willNotTimeout = time.Now().Add(time.Hour * 24 * 365 * 100) 175 | 176 | var unAckRcvSince = willNotTimeout 177 | var idleTimeout3Sine = time.Now() // 空闲间隔发起testFrAlive 178 | var testFrAliveSendSince = willNotTimeout // 当发起testFrAlive时,等待确认回复的超时间隔 179 | // 对于server端,无需对应的U-Frame 无需判断 180 | // var startDtActiveSendSince = willNotTimeout 181 | // var stopDtActiveSendSince = willNotTimeout 182 | 183 | sendSFrame := func(rcvSN uint16) { 184 | sf.Debug("TX sFrame %v", sAPCI{rcvSN}) 185 | sf.sendRaw <- newSFrame(rcvSN) 186 | } 187 | sendUFrame := func(which byte) { 188 | sf.Debug("TX uFrame %v", uAPCI{which}) 189 | sf.sendRaw <- newUFrame(which) 190 | } 191 | 192 | sendIFrame := func(asdu1 []byte) { 193 | seqNo := sf.seqNoSend 194 | 195 | iframe, err := newIFrame(seqNo, sf.seqNoRcv, asdu1) 196 | if err != nil { 197 | return 198 | } 199 | sf.ackNoRcv = sf.seqNoRcv 200 | sf.seqNoSend = (seqNo + 1) & 32767 201 | sf.pending = append(sf.pending, seqPending{seqNo & 32767, time.Now()}) 202 | 203 | sf.Debug("TX iFrame %v", iAPCI{seqNo, sf.seqNoRcv}) 204 | sf.sendRaw <- iframe 205 | } 206 | if sf.onConnection != nil { 207 | sf.onConnection(sf) 208 | } 209 | defer func() { 210 | sf.setConnectStatus(disconnected) 211 | checkTicker.Stop() 212 | _ = sf.conn.Close() // 连锁引发cancel 213 | sf.wg.Wait() 214 | if sf.connectionLost != nil { 215 | sf.connectionLost(sf) 216 | } 217 | sf.Debug("run stopped!") 218 | }() 219 | 220 | for { 221 | if isActive && seqNoCount(sf.ackNoSend, sf.seqNoSend) <= sf.config.SendUnAckLimitK { 222 | select { 223 | case o := <-sf.sendASDU: 224 | sendIFrame(o) 225 | idleTimeout3Sine = time.Now() 226 | continue 227 | case <-sf.ctx.Done(): 228 | return 229 | default: // make no block 230 | } 231 | } 232 | select { 233 | case <-sf.ctx.Done(): 234 | return 235 | case now := <-checkTicker.C: 236 | // check all timeouts 237 | if now.Sub(testFrAliveSendSince) >= sf.config.SendUnAckTimeout1 { 238 | // now.Sub(startDtActiveSendSince) >= t.SendUnAckTimeout1 || 239 | // now.Sub(stopDtActiveSendSince) >= t.SendUnAckTimeout1 || 240 | sf.Error("test frame alive confirm timeout t₁") 241 | return 242 | } 243 | // check oldest unacknowledged outbound 244 | if sf.ackNoSend != sf.seqNoSend && 245 | //now.Sub(sf.peek()) >= sf.SendUnAckTimeout1 { 246 | now.Sub(sf.pending[0].sendTime) >= sf.config.SendUnAckTimeout1 { 247 | sf.ackNoSend++ 248 | sf.Error("fatal transmission timeout t₁") 249 | return 250 | } 251 | 252 | // 确定最早发送的i-Frame是否超时,超时则回复sFrame 253 | if sf.ackNoRcv != sf.seqNoRcv && 254 | (now.Sub(unAckRcvSince) >= sf.config.RecvUnAckTimeout2 || 255 | now.Sub(idleTimeout3Sine) >= timeoutResolution) { 256 | sendSFrame(sf.seqNoRcv) 257 | sf.ackNoRcv = sf.seqNoRcv 258 | } 259 | 260 | // 空闲时间到,发送TestFrActive帧,保活 261 | if now.Sub(idleTimeout3Sine) >= sf.config.IdleTimeout3 { 262 | sendUFrame(uTestFrActive) 263 | testFrAliveSendSince = time.Now() 264 | idleTimeout3Sine = testFrAliveSendSince 265 | } 266 | 267 | case apdu := <-sf.rcvRaw: 268 | idleTimeout3Sine = time.Now() // 每收到一个i帧,S帧,U帧, 重置空闲定时器, t3 269 | apci, asduVal := parse(apdu) 270 | switch head := apci.(type) { 271 | case sAPCI: 272 | sf.Debug("RX sFrame %v", head) 273 | if !sf.updateAckNoOut(head.rcvSN) { 274 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 275 | return 276 | } 277 | 278 | case iAPCI: 279 | sf.Debug("RX iFrame %v", head) 280 | if !isActive { 281 | sf.Warn("station not active") 282 | break // not active, discard apdu 283 | } 284 | if !sf.updateAckNoOut(head.rcvSN) || head.sendSN != sf.seqNoRcv { 285 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 286 | return 287 | } 288 | 289 | sf.rcvASDU <- asduVal 290 | if sf.ackNoRcv == sf.seqNoRcv { // first unacked 291 | unAckRcvSince = time.Now() 292 | } 293 | 294 | sf.seqNoRcv = (sf.seqNoRcv + 1) & 32767 295 | if seqNoCount(sf.ackNoRcv, sf.seqNoRcv) >= sf.config.RecvUnAckLimitW { 296 | sendSFrame(sf.seqNoRcv) 297 | sf.ackNoRcv = sf.seqNoRcv 298 | } 299 | 300 | case uAPCI: 301 | sf.Debug("RX uFrame %v", head) 302 | switch head.function { 303 | case uStartDtActive: 304 | sendUFrame(uStartDtConfirm) 305 | isActive = true 306 | // case uStartDtConfirm: 307 | // isActive = true 308 | // startDtActiveSendSince = willNotTimeout 309 | case uStopDtActive: 310 | sendUFrame(uStopDtConfirm) 311 | isActive = false 312 | // case uStopDtConfirm: 313 | // isActive = false 314 | // stopDtActiveSendSince = willNotTimeout 315 | case uTestFrActive: 316 | sendUFrame(uTestFrConfirm) 317 | case uTestFrConfirm: 318 | testFrAliveSendSince = willNotTimeout 319 | default: 320 | sf.Error("illegal U-Frame functions[0x%02x] ignored", head.function) 321 | } 322 | } 323 | } 324 | } 325 | } 326 | 327 | // handlerLoop handler iFrame asdu 328 | func (sf *SrvSession) handlerLoop() { 329 | sf.Debug("handlerLoop started") 330 | defer func() { 331 | sf.wg.Done() 332 | sf.Debug("handlerLoop stopped") 333 | }() 334 | 335 | for { 336 | select { 337 | case <-sf.ctx.Done(): 338 | return 339 | case rawAsdu := <-sf.rcvASDU: 340 | asduPack := asdu.NewEmptyASDU(sf.params) 341 | if err := asduPack.UnmarshalBinary(rawAsdu); err != nil { 342 | sf.Error("asdu UnmarshalBinary failed,%+v", err) 343 | continue 344 | } 345 | if err := sf.serverHandler(asduPack); err != nil { 346 | sf.Error("serverHandler falied,%+v", err) 347 | } 348 | } 349 | } 350 | } 351 | 352 | func (sf *SrvSession) setConnectStatus(status uint32) { 353 | sf.rwMux.Lock() 354 | atomic.StoreUint32(&sf.status, status) 355 | sf.rwMux.Unlock() 356 | } 357 | 358 | func (sf *SrvSession) connectStatus() uint32 { 359 | sf.rwMux.RLock() 360 | status := atomic.LoadUint32(&sf.status) 361 | sf.rwMux.RUnlock() 362 | return status 363 | } 364 | 365 | func (sf *SrvSession) cleanUp() { 366 | sf.ackNoRcv = 0 367 | sf.ackNoSend = 0 368 | sf.seqNoRcv = 0 369 | sf.seqNoSend = 0 370 | sf.pending = nil 371 | // clear sending chan buffer 372 | loop: 373 | for { 374 | select { 375 | case <-sf.sendRaw: 376 | case <-sf.rcvRaw: 377 | case <-sf.rcvASDU: 378 | case <-sf.sendASDU: 379 | default: 380 | break loop 381 | } 382 | } 383 | } 384 | 385 | // 回绕机制 386 | func seqNoCount(nextAckNo, nextSeqNo uint16) uint16 { 387 | if nextAckNo > nextSeqNo { 388 | nextSeqNo += 32768 389 | } 390 | return nextSeqNo - nextAckNo 391 | } 392 | 393 | func (sf *SrvSession) updateAckNoOut(ackNo uint16) (ok bool) { 394 | if ackNo == sf.ackNoSend { 395 | return true 396 | } 397 | // new acks validate, ack 不能在 req seq 前面,出错 398 | if seqNoCount(sf.ackNoSend, sf.seqNoSend) < seqNoCount(ackNo, sf.seqNoSend) { 399 | return false 400 | } 401 | 402 | // confirm reception 403 | for i, v := range sf.pending { 404 | if v.seq == (ackNo - 1) { 405 | sf.pending = sf.pending[i+1:] 406 | break 407 | } 408 | } 409 | 410 | sf.ackNoSend = ackNo 411 | return true 412 | } 413 | 414 | func (sf *SrvSession) serverHandler(asduPack *asdu.ASDU) error { 415 | defer func() { 416 | if err := recover(); err != nil { 417 | sf.Critical("server handler %+v", err) 418 | } 419 | }() 420 | 421 | sf.Debug("ASDU %+v", asduPack) 422 | 423 | switch asduPack.Identifier.Type { 424 | case asdu.C_IC_NA_1: // InterrogationCmd 425 | if !(asduPack.Identifier.Coa.Cause == asdu.Activation || 426 | asduPack.Identifier.Coa.Cause == asdu.Deactivation) { 427 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 428 | } 429 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 430 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 431 | } 432 | ioa, qoi := asduPack.GetInterrogationCmd() 433 | if ioa != asdu.InfoObjAddrIrrelevant { 434 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 435 | } 436 | return sf.handler.InterrogationHandler(sf, asduPack, qoi) 437 | 438 | case asdu.C_CI_NA_1: // CounterInterrogationCmd 439 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 440 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 441 | } 442 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 443 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 444 | } 445 | ioa, qcc := asduPack.GetCounterInterrogationCmd() 446 | if ioa != asdu.InfoObjAddrIrrelevant { 447 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 448 | } 449 | return sf.handler.CounterInterrogationHandler(sf, asduPack, qcc) 450 | 451 | case asdu.C_RD_NA_1: // ReadCmd 452 | if asduPack.Identifier.Coa.Cause != asdu.Request { 453 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 454 | } 455 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 456 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 457 | } 458 | return sf.handler.ReadHandler(sf, asduPack, asduPack.GetReadCmd()) 459 | 460 | case asdu.C_CS_NA_1: // ClockSynchronizationCmd 461 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 462 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 463 | } 464 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 465 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 466 | } 467 | 468 | ioa, tm := asduPack.GetClockSynchronizationCmd() 469 | if ioa != asdu.InfoObjAddrIrrelevant { 470 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 471 | } 472 | return sf.handler.ClockSyncHandler(sf, asduPack, tm) 473 | 474 | case asdu.C_TS_NA_1: // TestCommand 475 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 476 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 477 | } 478 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 479 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 480 | } 481 | ioa, _ := asduPack.GetTestCommand() 482 | if ioa != asdu.InfoObjAddrIrrelevant { 483 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 484 | } 485 | return asduPack.SendReplyMirror(sf, asdu.ActivationCon) 486 | 487 | case asdu.C_RP_NA_1: // ResetProcessCmd 488 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 489 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 490 | } 491 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 492 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 493 | } 494 | ioa, qrp := asduPack.GetResetProcessCmd() 495 | if ioa != asdu.InfoObjAddrIrrelevant { 496 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 497 | } 498 | return sf.handler.ResetProcessHandler(sf, asduPack, qrp) 499 | case asdu.C_CD_NA_1: // DelayAcquireCommand 500 | if !(asduPack.Identifier.Coa.Cause == asdu.Activation || 501 | asduPack.Identifier.Coa.Cause == asdu.Spontaneous) { 502 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 503 | } 504 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 505 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 506 | } 507 | ioa, msec := asduPack.GetDelayAcquireCommand() 508 | if ioa != asdu.InfoObjAddrIrrelevant { 509 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 510 | } 511 | return sf.handler.DelayAcquisitionHandler(sf, asduPack, msec) 512 | } 513 | 514 | if err := sf.handler.ASDUHandler(sf, asduPack); err != nil { 515 | return asduPack.SendReplyMirror(sf, asdu.UnknownTypeID) 516 | } 517 | return nil 518 | } 519 | 520 | // IsConnected get server session connected state 521 | func (sf *SrvSession) IsConnected() bool { 522 | return sf.connectStatus() == connected 523 | } 524 | 525 | // Params get params 526 | func (sf *SrvSession) Params() *asdu.Params { 527 | return sf.params 528 | } 529 | 530 | // Send asdu frame 531 | func (sf *SrvSession) Send(u *asdu.ASDU) error { 532 | if !sf.IsConnected() { 533 | return ErrUseClosedConnection 534 | } 535 | data, err := u.MarshalBinary() 536 | if err != nil { 537 | return err 538 | } 539 | select { 540 | case sf.sendASDU <- data: 541 | default: 542 | return ErrBufferFulled 543 | } 544 | return nil 545 | } 546 | 547 | // UnderlyingConn got under net.conn 548 | func (sf *SrvSession) UnderlyingConn() net.Conn { 549 | return sf.conn 550 | } 551 | -------------------------------------------------------------------------------- /cs104/server_special.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 thinkgos (thinkgo@aliyun.com). All rights reserved. 2 | // Use of this source code is governed by a version 3 of the GNU General 3 | // Public License, license that can be found in the LICENSE file. 4 | 5 | package cs104 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "math/rand" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/thinkgos/go-iecp5/asdu" 15 | "github.com/thinkgos/go-iecp5/clog" 16 | ) 17 | 18 | // ServerSpecial server special interface 19 | type ServerSpecial interface { 20 | asdu.Connect 21 | 22 | IsConnected() bool 23 | IsClosed() bool 24 | Start() error 25 | Close() error 26 | 27 | SetOnConnectHandler(f func(c asdu.Connect)) 28 | SetConnectionLostHandler(f func(c asdu.Connect)) 29 | 30 | LogMode(enable bool) 31 | SetLogProvider(p clog.LogProvider) 32 | } 33 | 34 | type serverSpec struct { 35 | SrvSession 36 | option ClientOption 37 | closeCancel context.CancelFunc 38 | } 39 | 40 | // NewServerSpecial new special server 41 | func NewServerSpecial(handler ServerHandlerInterface, o *ClientOption) ServerSpecial { 42 | return &serverSpec{ 43 | SrvSession: SrvSession{ 44 | config: &o.config, 45 | params: &o.params, 46 | handler: handler, 47 | 48 | rcvASDU: make(chan []byte, 1024), 49 | sendASDU: make(chan []byte, 1024), 50 | rcvRaw: make(chan []byte, 1024), 51 | sendRaw: make(chan []byte, 1024), // may not block! 52 | 53 | Clog: clog.NewLogger("cs104 serverSpec => "), 54 | }, 55 | option: *o, 56 | } 57 | } 58 | 59 | // SetOnConnectHandler set on connect handler 60 | func (sf *serverSpec) SetOnConnectHandler(f func(conn asdu.Connect)) { 61 | sf.onConnection = f 62 | } 63 | 64 | // SetConnectionLostHandler set connection lost handler 65 | func (sf *serverSpec) SetConnectionLostHandler(f func(c asdu.Connect)) { 66 | sf.connectionLost = f 67 | } 68 | 69 | // Start start the server,and return quickly,if it nil,the server will disconnected background,other failed 70 | func (sf *serverSpec) Start() error { 71 | if sf.option.server == nil { 72 | return errors.New("empty remote server") 73 | } 74 | 75 | go sf.running() 76 | return nil 77 | } 78 | 79 | // 增加重连间隔 80 | func (sf *serverSpec) running() { 81 | var ctx context.Context 82 | 83 | sf.rwMux.Lock() 84 | if !atomic.CompareAndSwapUint32(&sf.status, initial, disconnected) { 85 | sf.rwMux.Unlock() 86 | return 87 | } 88 | ctx, sf.closeCancel = context.WithCancel(context.Background()) 89 | sf.rwMux.Unlock() 90 | defer sf.setConnectStatus(initial) 91 | 92 | for { 93 | select { 94 | case <-ctx.Done(): 95 | return 96 | default: 97 | } 98 | 99 | sf.Debug("connecting server %+v", sf.option.server) 100 | conn, err := openConnection(sf.option.server, sf.option.TLSConfig, sf.config.ConnectTimeout0) 101 | if err != nil { 102 | sf.Error("connect failed, %v", err) 103 | if !sf.option.autoReconnect { 104 | return 105 | } 106 | time.Sleep(sf.option.reconnectInterval) 107 | continue 108 | } 109 | sf.Debug("connect success") 110 | sf.conn = conn 111 | sf.run(ctx) 112 | sf.Debug("disconnected server %+v", sf.option.server) 113 | select { 114 | case <-ctx.Done(): 115 | return 116 | default: 117 | // 随机500ms-1s的重试,避免快速重试造成服务器许多无效连接 118 | time.Sleep(time.Millisecond * time.Duration(500+rand.Intn(500))) 119 | } 120 | } 121 | } 122 | 123 | func (sf *serverSpec) IsClosed() bool { 124 | return sf.connectStatus() == initial 125 | } 126 | 127 | func (sf *serverSpec) Close() error { 128 | sf.rwMux.Lock() 129 | if sf.closeCancel != nil { 130 | sf.closeCancel() 131 | } 132 | sf.rwMux.Unlock() 133 | return nil 134 | } 135 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thinkgos/go-iecp5 2 | 3 | go 1.15 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thinkgos/go-iecp5/e76e21af681ff89e46ad1c4b2fd2d4691b4059a9/go.sum --------------------------------------------------------------------------------