├── .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 | [](https://pkg.go.dev/github.com/thinkgos/go-iecp5?tab=doc)
11 | [](https://github.com/thinkgos/go-iecp5/actions/workflows/ci.yml)
12 | [](https://codecov.io/gh/thinkgos/go-iecp5)
13 | [](https://goreportcard.com/report/github.com/thinkgos/go-iecp5)
14 | [](https://github.com/thinkgos/go-iecp5/raw/master/LICENSE)
15 | [](https://github.com/thinkgos/go-iecp5/tags)
16 | [](https://sourcegraph.com/github.com/thinkgos/go-iecp5?badge)
17 |
18 |
19 | asdu package: [](https://godoc.org/github.com/thinkgos/go-iecp5/asdu)
20 | clog package: [](https://godoc.org/github.com/thinkgos/go-iecp5/clog)
21 | cs104 package: [](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 | 
39 |
40 | **WeChat Pay**
41 |
42 | 
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
--------------------------------------------------------------------------------