├── LICENSE ├── README.md ├── _examples └── iec61850_client_general │ └── cliGeneral.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 ├── go.mod ├── go.sum ├── iec61850 ├── 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 ├── revive.sh └── revive.toml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 jiang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-iec61850 2 | go-iec61850 3 | -------------------------------------------------------------------------------- /_examples/iec61850_client_general/cliGeneral.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/themeyic/go-iec61850/iec61850" 6 | ) 7 | 8 | type myClient struct{} 9 | 10 | func main() { 11 | var err error 12 | 13 | option := iec61850.NewOption() 14 | if err = option.AddRemoteServer("10.211.55.4:102"); err != nil { 15 | panic(err) 16 | } 17 | 18 | 19 | client := iec61850.NewClient( option) 20 | 21 | client.LogMode(true) 22 | 23 | client.SetOnConnectHandler(func(c *iec61850.Client) { 24 | c.SendStartDt() // 发送startDt激活指令 25 | }) 26 | err = client.Start() 27 | 28 | for{ 29 | select { 30 | case getValue := <-iec61850.Transfer : 31 | test := fmt.Sprintf("%v",getValue) 32 | fmt.Println("%x",test) 33 | return 34 | } 35 | } 36 | 37 | //if err != nil { 38 | // panic(fmt.Errorf("Failed to connect. error:%v\n", err)) 39 | //} 40 | // 41 | //for { 42 | // time.Sleep(time.Second * 100) 43 | //} 44 | 45 | } 46 | -------------------------------------------------------------------------------- /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.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 | "fmt" 9 | "strconv" 10 | ) 11 | 12 | // about data unit identification 应用服务数据单元 - 数据单元标识符 13 | 14 | // TypeID is the ASDU type identification. 15 | // See companion standard 101, subclass 7.2.1. 16 | type TypeID uint8 17 | 18 | // The standard ASDU type identification. 19 | // M for monitored information 20 | // C for control information 21 | // P for parameter 22 | // F for file transfer. 23 | // <0> 未用 24 | // <1..127> 标准定义 - 兼容 25 | // <128..135> 为路由报文保留 - 专用 26 | // <136..255> 特殊应用 - 专用 27 | // NOTE: 信息对象带或不带时标由标识符类型的不同序列来区别 28 | const ( 29 | _ TypeID = iota // 0: not defined 30 | // 在监视方向上的过程信息 <0..44> 31 | M_SP_NA_1 // 1: single-point information, 单点信息 32 | M_SP_TA_1 // 2: single-point information with time tag, 单点信息-带时标 33 | M_DP_NA_1 // 3: double-point information, 双点信息 34 | M_DP_TA_1 // 4: double-point information with time tag, 双点信息-带时标 35 | M_ST_NA_1 // 5: step position information, 步位置信息 36 | M_ST_TA_1 // 6: step position information with time tag, 步位置信息-带时标 37 | M_BO_NA_1 // 7: bitstring of 32 bit, 32位比特串 38 | M_BO_TA_1 // 8: bitstring of 32 bit with time tag, 32位比特串-带时标 39 | M_ME_NA_1 // 9: measured value, normalized value, 测量值,规一化值 40 | M_ME_TA_1 // 10: measured value, normalized value with time tag, 测量值,规一化值-带时标 41 | M_ME_NB_1 // 11: measured value, scaled value, 测量值,标度化值 42 | M_ME_TB_1 // 12: measured value, scaled value with time tag, 测量值带时标,标度化值-带时标 43 | M_ME_NC_1 // 13: measured value, short floating point number, 测量值,短浮点数 44 | M_ME_TC_1 // 14: measured value, short floating point number with time tag, 测量值,短浮数-带时标 45 | M_IT_NA_1 // 15: integrated totals, 累积量 46 | M_IT_TA_1 // 16: integrated totals with time tag, 累积量带时标 47 | M_EP_TA_1 // 17: event of protection equipment with time tag, 继电器保护设备事件-带时标 48 | M_EP_TB_1 // 18: packed start events of protection equipment with time tag, 继电保护设备成组启动事件-带时标 49 | M_EP_TC_1 // 19: packed output circuit information of protection equipment with time tag, 继电保护设备成组输出电路信息-带时标 50 | M_PS_NA_1 // 20: packed single-point information with status change detection, 带变位检出的成组单点信息 51 | M_ME_ND_1 // 21: measured value, normalized value without quality descriptor, 测量值,不带品质描述词的规一化值 52 | _ // 22: reserved for further compatible definitions 53 | _ // 23: reserved for further compatible definitions 54 | _ // 24: reserved for further compatible definitions 55 | _ // 25: reserved for further compatible definitions 56 | _ // 26: reserved for further compatible definitions 57 | _ // 27: reserved for further compatible definitions 58 | _ // 28: reserved for further compatible definitions 59 | _ // 29: reserved for further compatible definitions 60 | M_SP_TB_1 // 30: single-point information with time tag CP56Time2a, 单点信息-带CP56Time2a 61 | M_DP_TB_1 // 31: double-point information with time tag CP56Time2a, 双点信息-带CP56Time2a 62 | M_ST_TB_1 // 32: step position information with time tag CP56Time2a, 步位置信息-带CP56Time2a 63 | M_BO_TB_1 // 33: bitstring of 32 bits with time tag CP56Time2a, 32比特串-带CP56Time2a 64 | M_ME_TD_1 // 34: measured value, normalized value with time tag CP56Time2a, 测量值,规一化值-带CP56Time2a 65 | M_ME_TE_1 // 35: measured value, scaled value with time tag CP56Time2a, 测量值,标度化值-带CP56Time2a 66 | M_ME_TF_1 // 36: measured value, short floating point number with time tag CP56Time2a, 测量值,短浮点数-带CP56Time2a 67 | M_IT_TB_1 // 37: integrated totals with time tag CP56Time2a, 累积值-带CP56Time2a 68 | M_EP_TD_1 // 38: event of protection equipment with time tag CP56Time2a, 继电保护装置事件-带CP56Time2a 69 | M_EP_TE_1 // 39: packed start events of protection equipment with time tag CP56Time2a, 继电保护装置成组启动事件-带CP56Time2a 70 | M_EP_TF_1 // 40: packed output circuit information of protection equipment with time tag CP56Time2a, 继电保护装置成组输出电路信息-带CP56Time2a 71 | S_IT_TC_1 // 41: integrated totals containing time-tagged security statistics 72 | _ // 42: reserved for further compatible definitions 73 | _ // 43: reserved for further compatible definitions 74 | _ // 44: reserved for further compatible definitions 75 | // 在控制方向的过程信息 <45..69> 76 | C_SC_NA_1 // 45: single command 单点命令 77 | C_DC_NA_1 // 46: double command 双点命令 78 | C_RC_NA_1 // 47: regulating step command 调节步命令 79 | C_SE_NA_1 // 48: set-point command, normalized value 设定值命令,归一化值 80 | C_SE_NB_1 // 49: set-point command, scaled value 设定值命令,规度化值 81 | C_SE_NC_1 // 50: set-point command, short floating point number 设定值命令,短浮点数值 82 | C_BO_NA_1 // 51: bitstring of 32 bits 23位比特串 83 | _ // 52: reserved for further compatible definitions 84 | _ // 53: reserved for further compatible definitions 85 | _ // 54: reserved for further compatible definitions 86 | _ // 55: reserved for further compatible definitions 87 | _ // 56: reserved for further compatible definitions 88 | _ // 57: reserved for further compatible definitions 89 | C_SC_TA_1 // 58: single command with time tag CP56Time2a 90 | C_DC_TA_1 // 59: double command with time tag CP56Time2a 91 | C_RC_TA_1 // 60: regulating step command with time tag CP56Time2a 92 | C_SE_TA_1 // 61: set-point command with time tag CP56Time2a, normalized value 93 | C_SE_TB_1 // 62: set-point command with time tag CP56Time2a, scaled value 94 | C_SE_TC_1 // 63: set-point command with time tag CP56Time2a, short floating point number 95 | C_BO_TA_1 // 64: bitstring of 32-bit with time tag CP56Time2a 96 | _ // 65: reserved for further compatible definitions 97 | _ // 66: reserved for further compatible definitions 98 | _ // 67: reserved for further compatible definitions 99 | _ // 68: reserved for further compatible definitions 100 | _ // 69: reserved for further compatible definitions 101 | // 在监视方向的系统命令 <70..99> 102 | M_EI_NA_1 // 70: end of initialization 初始化结束 103 | _ // 71: reserved for further compatible definitions 104 | _ // 72: reserved for further compatible definitions 105 | _ // 73: reserved for further compatible definitions 106 | _ // 74: reserved for further compatible definitions 107 | _ // 75: reserved for further compatible definitions 108 | _ // 76: reserved for further compatible definitions 109 | _ // 77: reserved for further compatible definitions 110 | _ // 78: reserved for further compatible definitions 111 | _ // 79: reserved for further compatible definitions 112 | _ // 80: reserved for further compatible definitions 113 | S_CH_NA_1 // 81: authentication challenge 114 | S_RP_NA_1 // 82: authentication reply 115 | S_AR_NA_1 // 83: aggressive mode authentication request 116 | S_KR_NA_1 // 84: session key status request 117 | S_KS_NA_1 // 85: session key status 118 | S_KC_NA_1 // 86: session key change 119 | S_ER_NA_1 // 87: authentication error 120 | _ // 88: reserved for further compatible definitions 121 | _ // 89: reserved for further compatible definitions 122 | S_US_NA_1 // 90: user status change 123 | S_UQ_NA_1 // 91: update key change request 124 | S_UR_NA_1 // 92: update key change reply 125 | S_UK_NA_1 // 93: update key change — symetric 126 | S_UA_NA_1 // 94: update key change — asymetric 127 | S_UC_NA_1 // 95: update key change confirmation 128 | _ // 96: reserved for further compatible definitions 129 | _ // 97: reserved for further compatible definitions 130 | _ // 98: reserved for further compatible definitions 131 | _ // 99: reserved for further compatible definitions 132 | // 在控制方向的系统命令 <100..109> 133 | C_IC_NA_1 // 100: interrogation command 总召唤 134 | C_CI_NA_1 // 101: counter interrogation command 计数量召唤 135 | C_RD_NA_1 // 102: read command 读命令 136 | C_CS_NA_1 // 103: clock synchronization command 时钟同步命令 137 | C_TS_NA_1 // 104: test command 测试命令 138 | C_RP_NA_1 // 105: reset process command 复位进程命令 139 | C_CD_NA_1 // 106: delay acquisition command 延时获得命令 140 | C_TS_TA_1 // 107: test command with time tag CP56Time2a 带CP56Time2a的测试命令 141 | _ // 108: reserved for further compatible definitions 142 | _ // 109: reserved for further compatible definitions 143 | // 在控制方向的参数命令 <110..119> 144 | P_ME_NA_1 // 110: parameter of measured value, normalized value 测量值参数,规一化值 145 | P_ME_NB_1 // 111: parameter of measured value, scaled value 测量值参数,标度化值 146 | P_ME_NC_1 // 112: parameter of measured value, short floating point number 测量值参数,短浮点数 147 | P_AC_NA_1 // 113: parameter activation 参数激活 148 | _ // 114: reserved for further compatible definitions 149 | _ // 115: reserved for further compatible definitions 150 | _ // 116: reserved for further compatible definitions 151 | _ // 117: reserved for further compatible definitions 152 | _ // 118: reserved for further compatible definitions 153 | _ // 119: reserved for further compatible definitions 154 | // 文件传输 <120..127> 155 | F_FR_NA_1 // 120: file ready 文件准备就绪 156 | F_SR_NA_1 // 121: section ready 节准备就绪 157 | F_SC_NA_1 // 122: call directory, select file, call file, call section 如唤目录,选择文件,召唤文件,召唤节 158 | F_LS_NA_1 // 123: last section, last segment 最后的节,最后的段 159 | F_AF_NA_1 // 124: ack file, ack section 认可文件,认可节 160 | F_SG_NA_1 // 125: segment 段 161 | F_DR_TA_1 // 126: directory 目录 162 | F_SC_NB_1 // 127: QueryLog - request archive file (section 104) 查询日志 163 | ) 164 | 165 | // infoObjSize maps the type identification (TypeID) to the serial octet size. 166 | // Type extensions must register here. 167 | var infoObjSize = [256]int{ 168 | M_SP_NA_1: 1, 169 | M_SP_TA_1: 4, 170 | M_DP_NA_1: 1, 171 | M_DP_TA_1: 4, 172 | M_ST_NA_1: 2, 173 | M_ST_TA_1: 5, 174 | M_BO_NA_1: 5, 175 | M_BO_TA_1: 8, 176 | M_ME_NA_1: 3, 177 | M_ME_TA_1: 6, 178 | M_ME_NB_1: 3, 179 | M_ME_TB_1: 6, 180 | M_ME_NC_1: 5, 181 | M_ME_TC_1: 8, 182 | M_IT_NA_1: 5, 183 | M_IT_TA_1: 8, 184 | M_EP_TA_1: 6, 185 | M_EP_TB_1: 7, 186 | M_EP_TC_1: 7, 187 | M_PS_NA_1: 5, 188 | M_ME_ND_1: 2, 189 | 190 | M_SP_TB_1: 8, 191 | M_DP_TB_1: 8, 192 | M_ST_TB_1: 9, 193 | M_BO_TB_1: 12, 194 | M_ME_TD_1: 10, 195 | M_ME_TE_1: 10, 196 | M_ME_TF_1: 12, 197 | M_IT_TB_1: 12, 198 | M_EP_TD_1: 11, 199 | M_EP_TE_1: 11, 200 | M_EP_TF_1: 11, 201 | 202 | C_SC_NA_1: 1, 203 | C_DC_NA_1: 1, 204 | C_RC_NA_1: 1, 205 | C_SE_NA_1: 3, 206 | C_SE_NB_1: 3, 207 | C_SE_NC_1: 5, 208 | C_BO_NA_1: 4, 209 | 210 | M_EI_NA_1: 1, 211 | 212 | C_IC_NA_1: 1, 213 | C_CI_NA_1: 1, 214 | C_RD_NA_1: 0, 215 | C_CS_NA_1: 7, 216 | C_TS_NA_1: 2, 217 | C_RP_NA_1: 1, 218 | C_CD_NA_1: 2, 219 | 220 | P_ME_NA_1: 3, 221 | P_ME_NB_1: 3, 222 | P_ME_NC_1: 5, 223 | P_AC_NA_1: 1, 224 | 225 | F_FR_NA_1: 6, 226 | F_SR_NA_1: 7, 227 | F_SC_NA_1: 4, 228 | F_LS_NA_1: 5, 229 | F_AF_NA_1: 4, 230 | // F_SG_NA_1: 4 + variable, 231 | F_DR_TA_1: 13, 232 | } 233 | 234 | // GetInfoObjSize get the serial octet size of the type identification (TypeID). 235 | func GetInfoObjSize(id TypeID) (int, error) { 236 | size := infoObjSize[id] 237 | if size == 0 { 238 | return 0, ErrTypeIdentifier 239 | } 240 | return size, nil 241 | } 242 | 243 | const ( 244 | _TypeIDName0 = "M_SP_NA_1M_SP_TA_1M_DP_NA_1M_DP_TA_1M_ST_NA_1M_ST_TA_1M_BO_NA_1M_BO_TA_1M_ME_NA_1M_ME_TA_1M_ME_NB_1M_ME_TB_1M_ME_NC_1M_ME_TC_1M_IT_NA_1M_IT_TA_1M_EP_TA_1M_EP_TB_1M_EP_TC_1M_PS_NA_1M_ME_ND_1" 245 | _TypeIDName1 = "M_SP_TB_1M_DP_TB_1M_ST_TB_1M_BO_TB_1M_ME_TD_1M_ME_TE_1M_ME_TF_1M_IT_TB_1M_EP_TD_1M_EP_TE_1M_EP_TF_1S_IT_TC_1" 246 | _TypeIDName2 = "C_SC_NA_1C_DC_NA_1C_RC_NA_1C_SE_NA_1C_SE_NB_1C_SE_NC_1C_BO_NA_1" 247 | _TypeIDName3 = "C_SC_TA_1C_DC_TA_1C_RC_TA_1C_SE_TA_1C_SE_TB_1C_SE_TC_1C_BO_TA_1" 248 | _TypeIDName4 = "M_EI_NA_1" 249 | _TypeIDName5 = "S_CH_NA_1S_RP_NA_1S_AR_NA_1S_KR_NA_1S_KS_NA_1S_KC_NA_1S_ER_NA_1" 250 | _TypeIDName6 = "S_US_NA_1S_UQ_NA_1S_UR_NA_1S_UK_NA_1S_UA_NA_1S_UC_NA_1" 251 | _TypeIDName7 = "C_IC_NA_1C_CI_NA_1C_RD_NA_1C_CS_NA_1C_TS_NA_1C_RP_NA_1C_CD_NA_1C_TS_TA_1" 252 | _TypeIDName8 = "P_ME_NA_1P_ME_NB_1P_ME_NC_1P_AC_NA_1" 253 | _TypeIDName9 = "F_FR_NA_1F_SR_NA_1F_SC_NA_1F_LS_NA_1F_AF_NA_1F_SG_NA_1F_DR_TA_1F_SC_NB_1" 254 | ) 255 | 256 | func (sf TypeID) String() string { 257 | var s string 258 | switch { 259 | case 1 <= sf && sf <= 21: 260 | sf-- 261 | s = _TypeIDName0[sf*9 : 9*(sf+1)] 262 | case 30 <= sf && sf <= 41: 263 | sf -= 30 264 | s = _TypeIDName1[sf*9 : 9*(sf+1)] 265 | case 45 <= sf && sf <= 51: 266 | sf -= 45 267 | s = _TypeIDName2[sf*9 : 9*(sf+1)] 268 | case 58 <= sf && sf <= 64: 269 | sf -= 58 270 | s = _TypeIDName3[sf*9 : 9*(sf+1)] 271 | case sf == 70: 272 | s = _TypeIDName4 273 | case 81 <= sf && sf <= 87: 274 | sf -= 81 275 | s = _TypeIDName5[sf*9 : 9*(sf+1)] 276 | case 90 <= sf && sf <= 95: 277 | sf -= 90 278 | s = _TypeIDName6[sf*9 : 9*(sf+1)] 279 | case 100 <= sf && sf <= 107: 280 | sf -= 100 281 | s = _TypeIDName7[sf*9 : 9*(sf+1)] 282 | case 110 <= sf && sf <= 113: 283 | sf -= 110 284 | s = _TypeIDName8[sf*9 : 9*(sf+1)] 285 | case 120 <= sf && sf <= 127: 286 | sf -= 120 287 | s = _TypeIDName9[sf*9 : 9*(sf+1)] 288 | default: 289 | s = strconv.FormatInt(int64(sf), 10) 290 | } 291 | return "TID<" + s + ">" 292 | } 293 | 294 | // VariableStruct is variable structure qualifier 295 | // See companion standard 101, subclass 7.2.2. 296 | // number <0..127>: bit0 - bit6 297 | // seq: bit7 298 | // 0: 同一类型,有不同objAddress的信息元素集合 (地址+元素)*N 299 | // 1: 同一类型,相同objAddress顺序信息元素集合 (一个地址,N元素*N) 300 | type VariableStruct struct { 301 | Number byte 302 | IsSequence bool 303 | } 304 | 305 | // ParseVariableStruct parse byte to variable structure qualifier 306 | func ParseVariableStruct(b byte) VariableStruct { 307 | return VariableStruct{ 308 | Number: b & 0x7f, 309 | IsSequence: (b & 0x80) == 0x80, 310 | } 311 | } 312 | 313 | // Value encode variable structure to byte 314 | func (sf VariableStruct) Value() byte { 315 | if sf.IsSequence { 316 | return sf.Number | 0x80 317 | } 318 | return sf.Number 319 | } 320 | 321 | // String 返回 variable structure 的格式 322 | func (sf VariableStruct) String() string { 323 | if sf.IsSequence { 324 | return fmt.Sprintf("VSQ", sf.Number) 325 | } 326 | return fmt.Sprintf("VSQ<%d>", sf.Number) 327 | } 328 | 329 | // CauseOfTransmission is the cause of transmission. 330 | // See companion standard 101, subclass 7.2.3. 331 | // | T | P/N | 5..0 cause | 332 | // T = test, the cause of transmission for testing ,0: 未试验, 1:试验 333 | // P/N indicates the negative (or positive) confirmation. 334 | // Cause is the cause of transmission. bit5 - bit0 335 | // 对由启动应用功能所请求的激活以肯定或者否定的确认 0: 肯定确认, 1: 否定确认 336 | type CauseOfTransmission struct { 337 | IsTest bool 338 | IsNegative bool 339 | Cause Cause 340 | } 341 | 342 | // OriginAddr is originator address, See companion standard 101, subclass 7.2.3. 343 | // The width is controlled by Params.CauseSize. width 2 includes/activates the originator address. 344 | // <0>: 未用 345 | // <1..255>: 源发地址 346 | type OriginAddr byte 347 | 348 | // Cause is the cause of transmission. bit5-bit0 349 | type Cause byte 350 | 351 | // Cause of transmission bit5-bit0 352 | // <0> 未定义 353 | // <1..63> 传输原因序号 354 | // <1..47> 标准定义 355 | // <48..63> 专用范围 356 | // NOTE: 信息对象带或不带时标由标识符类型的不同序列来区别 357 | const ( 358 | Unused Cause = iota // unused 359 | Periodic // periodic, cyclic 360 | Background // background scan 361 | Spontaneous // spontaneous 突发 362 | Initialized // initialized 363 | Request // request or requested 364 | Activation // activation 激活 365 | ActivationCon // activation confirmation 激活确认 366 | Deactivation // deactivation 停止激活 367 | DeactivationCon // deactivation confirmation 停止激活确认 368 | ActivationTerm // activation termination 激活停止 369 | ReturnInfoRemote // return information caused by a remote command 370 | ReturnInfoLocal // return information caused by a local command 371 | FileTransfer // file transfer 372 | Authentication // authentication 373 | SessionKey // maintenance of authentication session key 374 | UserRoleAndUpdateKey // maintenance of user role and update key 375 | _ // reserved for further compatible definitions 376 | _ // reserved for further compatible definitions 377 | _ // reserved for further compatible definitions 378 | InterrogatedByStation // interrogated by station interrogation 379 | InterrogatedByGroup1 // interrogated by group 1 interrogation 380 | InterrogatedByGroup2 // interrogated by group 2 interrogation 381 | InterrogatedByGroup3 // interrogated by group 3 interrogation 382 | InterrogatedByGroup4 // interrogated by group 4 interrogation 383 | InterrogatedByGroup5 // interrogated by group 5 interrogation 384 | InterrogatedByGroup6 // interrogated by group 6 interrogation 385 | InterrogatedByGroup7 // interrogated by group 7 interrogation 386 | InterrogatedByGroup8 // interrogated by group 8 interrogation 387 | InterrogatedByGroup9 // interrogated by group 9 interrogation 388 | InterrogatedByGroup10 // interrogated by group 10 interrogation 389 | InterrogatedByGroup11 // interrogated by group 11 interrogation 390 | InterrogatedByGroup12 // interrogated by group 12 interrogation 391 | InterrogatedByGroup13 // interrogated by group 13 interrogation 392 | InterrogatedByGroup14 // interrogated by group 14 interrogation 393 | InterrogatedByGroup15 // interrogated by group 15 interrogation 394 | InterrogatedByGroup16 // interrogated by group 16 interrogation 395 | RequestByGeneralCounter // requested by general counter request 396 | RequestByGroup1Counter // requested by group 1 counter request 397 | RequestByGroup2Counter // requested by group 2 counter request 398 | RequestByGroup3Counter // requested by group 3 counter request 399 | RequestByGroup4Counter // requested by group 4 counter request 400 | _ // reserved for further compatible definitions 401 | _ // reserved for further compatible definitions 402 | UnknownTypeID // unknown type identification 403 | UnknownCOT // unknown cause of transmission 404 | UnknownCA // unknown common address of ASDU 405 | UnknownIOA // unknown information object address 406 | ) 407 | 408 | // Causal semantics description 409 | var causeSemantics = []string{ 410 | "Unused0", 411 | "Periodic", 412 | "Background", 413 | "Spontaneous", 414 | "Initialized", 415 | "Request", 416 | "Activation", 417 | "ActivationCon", 418 | "Deactivation", 419 | "DeactivationCon", 420 | "ActivationTerm", 421 | "ReturnInfoRemote", 422 | "ReturnInfoLocal", 423 | "FileTransfer", 424 | "Authentication", 425 | "SessionKey", 426 | "UserRoleAndUpdateKey", 427 | "Reserved17", 428 | "Reserved18", 429 | "Reserved19", 430 | "InterrogatedByStation", 431 | "InterrogatedByGroup1", 432 | "InterrogatedByGroup2", 433 | "InterrogatedByGroup3", 434 | "InterrogatedByGroup4", 435 | "InterrogatedByGroup5", 436 | "InterrogatedByGroup6", 437 | "InterrogatedByGroup7", 438 | "InterrogatedByGroup8", 439 | "InterrogatedByGroup9", 440 | "InterrogatedByGroup10", 441 | "InterrogatedByGroup11", 442 | "InterrogatedByGroup12", 443 | "InterrogatedByGroup13", 444 | "InterrogatedByGroup14", 445 | "InterrogatedByGroup15", 446 | "InterrogatedByGroup16", 447 | "RequestByGeneralCounter", 448 | "RequestByGroup1Counter", 449 | "RequestByGroup2Counter", 450 | "RequestByGroup3Counter", 451 | "RequestByGroup4Counter", 452 | "Reserved42", 453 | "Reserved43", 454 | "UnknownTypeID", 455 | "UnknownCOT", 456 | "UnknownCA", 457 | "UnknownIOA", 458 | "Special48", 459 | "Special49", 460 | "Special50", 461 | "Special51", 462 | "Special52", 463 | "Special53", 464 | "Special54", 465 | "Special55", 466 | "Special56", 467 | "Special57", 468 | "Special58", 469 | "Special59", 470 | "Special60", 471 | "Special61", 472 | "Special62", 473 | "Special63", 474 | } 475 | 476 | // ParseCauseOfTransmission parse byte to cause of transmission 477 | func ParseCauseOfTransmission(b byte) CauseOfTransmission { 478 | return CauseOfTransmission{ 479 | IsNegative: (b & 0x40) == 0x40, 480 | IsTest: (b & 0x80) == 0x80, 481 | Cause: Cause(b & 0x3f), 482 | } 483 | } 484 | 485 | // Value encode cause of transmission to byte 486 | func (sf CauseOfTransmission) Value() byte { 487 | v := sf.Cause 488 | if sf.IsNegative { 489 | v |= 0x40 490 | } 491 | if sf.IsTest { 492 | v |= 0x80 493 | } 494 | return byte(v) 495 | } 496 | 497 | // String 返回Cause的字符串,包含相应应用的",neg" and ",test" 498 | func (sf CauseOfTransmission) String() string { 499 | s := "COT<" + causeSemantics[sf.Cause] 500 | switch { 501 | case sf.IsNegative && sf.IsTest: 502 | s += ",neg,test" 503 | case sf.IsNegative: 504 | s += ",neg" 505 | case sf.IsTest: 506 | s += ",test" 507 | } 508 | return s + ">" 509 | } 510 | 511 | // CommonAddr is a station address. 512 | // The width is controlled by Params.CommonAddrSize. 513 | // width 1: 514 | // <0>: 未用 515 | // <1..254>: 站地址 516 | // <255>: 全局地址 517 | // width 2: 518 | // <0>: 未用 519 | // <1..65534>: 站地址 520 | // <65535>: 全局地址 521 | type CommonAddr uint16 522 | 523 | // special commonAddr 524 | const ( 525 | // InvalidCommonAddr is the invalid common address. 526 | InvalidCommonAddr CommonAddr = 0 527 | // GlobalCommonAddr is the broadcast address. Use is restricted 528 | // to C_IC_NA_1, C_CI_NA_1, C_CS_NA_1 and C_RP_NA_1. 529 | // When in 8-bit mode 255 is mapped to this value on the fly. 530 | GlobalCommonAddr CommonAddr = 65535 531 | ) 532 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/themeyic/go-iec61850 2 | 3 | go 1.14 -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/themeyic/go-iec104 v0.0.0-20201009064327-0c93402ab811 h1:6pt2XbhCSeYBPEsBGQbo0W0Z89RVkoPaw3CzU8ClHX4= 2 | github.com/themeyic/go-iec104 v0.0.0-20201009064327-0c93402ab811/go.mod h1:1EpdwmZQQf3tQEhNZLm8qIVxhhxJ885zpqcGpIWmq1o= 3 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | 11 | "github.com/themeyic/go-iec61850/asdu" 12 | ) 13 | 14 | const startFrame byte = 0x68 // 启动字符 15 | 16 | // APDU form Max size 255 17 | // | APCI | ASDU | 18 | // | start | APDU length | control field | ASDU | 19 | // | APDU field size(253) | 20 | // bytes| 1 | 1 | 4 | | 21 | const ( 22 | APCICtlFiledSize = 4 // control filed(4) 23 | 24 | APDUSizeMax = 255 // start(1) + length(1) + control field(4) + ASDU 25 | APDUFieldSizeMax = APCICtlFiledSize + asdu.ASDUSizeMax // control field(4) + ASDU 26 | ) 27 | 28 | // U帧 控制域功能 29 | const ( 30 | uStartDtActive byte = 4 << iota // 启动激活 0x04 31 | uStartDtConfirm // 启动确认 0x08 32 | uStopDtActive // 停止激活 0x10 33 | uStopDtConfirm // 停止确认 0x20 34 | uTestFrActive // 测试激活 0x40 35 | uTestFrConfirm // 测试确认 0x80 36 | ) 37 | 38 | // I帧 含apci和asdu 信息帧.用于编号的信息传输 information 39 | type iAPCI struct { 40 | sendSN, rcvSN uint16 41 | } 42 | 43 | func (sf iAPCI) String() string { 44 | return fmt.Sprintf("I[sendNO: %d, recvNO: %d]", sf.sendSN, sf.rcvSN) 45 | } 46 | 47 | // S帧 只含apci S帧用于主要用确认帧的正确传输,协议称是监视. supervisory 48 | type sAPCI struct { 49 | rcvSN uint16 50 | } 51 | 52 | func (sf sAPCI) String() string { 53 | return fmt.Sprintf("S[recvNO: %d]", sf.rcvSN) 54 | } 55 | 56 | //U帧 只含apci 未编号控制信息 unnumbered 57 | type uAPCI struct { 58 | function byte // bit8 测试确认 59 | } 60 | 61 | func (sf uAPCI) String() string { 62 | var s string 63 | switch sf.function { 64 | case uStartDtActive: 65 | s = "StartDtActive" 66 | case uStartDtConfirm: 67 | s = "StartDtConfirm" 68 | case uStopDtActive: 69 | s = "StopDtActive" 70 | case uStopDtConfirm: 71 | s = "StopDtConfirm" 72 | case uTestFrActive: 73 | s = "TestFrActive" 74 | case uTestFrConfirm: 75 | s = "TestFrConfirm" 76 | default: 77 | s = "Unknown" 78 | } 79 | return fmt.Sprintf("U[function: %s]", s) 80 | } 81 | 82 | // newIFrame 创建I帧 ,返回apdu 83 | func newIFrame(sendSN, RcvSN uint16, asdus []byte) ([]byte, error) { 84 | if len(asdus) > asdu.ASDUSizeMax { 85 | return nil, fmt.Errorf("ASDU filed large than max %d", asdu.ASDUSizeMax) 86 | } 87 | 88 | b := make([]byte, len(asdus)+6) 89 | 90 | b[0] = startFrame 91 | b[1] = byte(len(asdus) + 4) 92 | b[2] = byte(sendSN << 1) 93 | b[3] = byte(sendSN >> 7) 94 | b[4] = byte(RcvSN << 1) 95 | b[5] = byte(RcvSN >> 7) 96 | copy(b[6:], asdus) 97 | 98 | return b, nil 99 | } 100 | 101 | // newSFrame 创建S帧,返回apdu 102 | func newSFrame(RcvSN uint16) []byte { 103 | return []byte{startFrame, 4, 0x01, 0x00, byte(RcvSN << 1), byte(RcvSN >> 7)} 104 | } 105 | 106 | 107 | 108 | type GooseDOApp struct { 109 | GocbRef string `asn1:"tag:0"` 110 | TimeToLive int `asn1:"tag:1"` 111 | DataSet string `asn1:"tag:2"` 112 | GoID string `asn1:"optional,tag:3"` 113 | UtcTime [8]byte `asn1:"tag:4"` 114 | StNum int `asn1:"tag:5"` 115 | SqNum int `asn1:"tag:6"` 116 | Test bool `asn1:"tag:7"` 117 | ConfRev int `asn1:"tag:8"` 118 | NeedsCommissioning bool `asn1:"tag:9"` 119 | NumDataSetEntries int `asn1:"tag:10"` 120 | AllData AllDataDO `asn1:"tag:11"` 121 | } 122 | 123 | type AllDataDO struct { 124 | DO0 bool `asn1:"tag:3"` 125 | DO1 bool `asn1:"tag:3"` 126 | DO2 bool `asn1:"tag:3"` 127 | DO3 bool `asn1:"tag:3"` 128 | DO4 bool `asn1:"tag:3"` 129 | DO5 bool `asn1:"tag:3"` 130 | DO6 bool `asn1:"tag:3"` 131 | DO7 bool `asn1:"tag:3"` 132 | } 133 | 134 | 135 | const ( 136 | GOOSE_TYPE_ID uint32 = 35000 //[2]byte{0x88, 0xb8} 137 | SV_TYPE_ID uint32 = 35002 //[2]byte{0x88, 0xba} 138 | ) 139 | 140 | // 以太网报文头 141 | type EtherHeader struct { 142 | DstHwaddr []byte 143 | LocHwaddr []byte 144 | VlanTag []byte 145 | TypeId uint32 146 | AppId uint32 147 | } 148 | 149 | func PackEtherPacket(header EtherHeader, apdu []byte) []byte { 150 | if header.TypeId != GOOSE_TYPE_ID && header.TypeId != SV_TYPE_ID { 151 | log.Println("PackEtherPacket: not goose or sv packet.") 152 | return nil 153 | } 154 | 155 | packet := make([]byte, 26+len(apdu)) 156 | idx := 0 157 | copy(packet[idx:], header.DstHwaddr) 158 | idx += 6 159 | copy(packet[idx:], header.LocHwaddr) 160 | idx += 6 161 | if len(header.VlanTag) != 0 { 162 | copy(packet[idx:], header.VlanTag) 163 | idx += 4 164 | } 165 | EncodeUint(header.TypeId, packet[idx:idx+2]) 166 | idx += 2 167 | EncodeUint(header.AppId, packet[idx:idx+2]) 168 | idx += 2 169 | EncodeUint(uint32(len(apdu)+8), packet[idx:idx+2]) 170 | idx += 2 171 | EncodeUint(0, packet[idx:idx+4]) 172 | idx += 4 173 | copy(packet[idx:], apdu) 174 | idx += len(apdu) 175 | 176 | return packet[:idx] 177 | } 178 | 179 | func EncodeUint(iVal uint32, b []byte) { 180 | b_len := len(b) 181 | if b_len > 4 { 182 | b_len = 4 183 | } 184 | for i := 0; i < b_len; i++ { 185 | shift := 8 * uint(b_len-i-1) 186 | mask := uint32(0xff) << shift 187 | b[i] = byte((iVal & mask) >> shift) 188 | } 189 | } 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | // newUFrame 创建U帧,返回apdu 200 | func newInitFrame() []byte { 201 | //return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 202 | startMark = 0 203 | endMark = 20 204 | return []byte{0x03,0x00,0x00,0x16,0x11,0xe0,0x00,0x00,0x00,0x01,0x00,0xc0,0x01,0x0a,0xc2,0x02,0x00,0x01,0xc1,0x02,0x00,0x01} 205 | 206 | //return []byte{0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 207 | } 208 | 209 | func newSecondUFrame() []byte { 210 | //return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 211 | //return []byte{0x03,0x00,0x00,0x16,0x11,0xe0,0x00,0x00,0x00,0x01,0x00,0xc0,0x01,0x0a,0xc2,0x02,0x00,0x01,0xc1,0x02,0x00,0x01} 212 | startMark = 0 213 | endMark = 20 214 | return []byte{0x03,0x00,0x00,0xba,0x02,0xf0,0x80,0x0d,0xb1,0x05,0x06,0x13,0x01,0x00,0x16,0x01,0x02,0x14,0x02,0x00,0x02,0x33,0x02,0x00,0x01,0x34,0x02,0x00,0x01,0xc1,0x9b,0x31,0x81,0x98,0xa0,0x03,0x80,0x01,0x01,0xa2,0x81,0x90,0x81,0x04,0x00,0x00,0x00,0x01,0x82,0x04,0x00,0x00,0x00,0x01,0xa4,0x23,0x30,0x0f,0x02,0x01,0x01,0x06,0x04,0x52,0x01,0x00,0x01,0x30,0x04,0x06,0x02,0x51,0x01,0x30,0x10,0x02,0x01,0x03,0x06,0x05,0x28,0xca,0x22,0x02,0x01,0x30,0x04,0x06,0x02,0x51,0x01,0x61,0x5d,0x30,0x5b,0x02,0x01,0x01,0xa0,0x56,0x60,0x54,0xa1,0x07,0x06,0x05,0x28,0xca,0x22,0x02,0x03,0xa2,0x06,0x06,0x04,0x2b,0xce,0x0f,0x0d,0xa3,0x03,0x02,0x01,0x0c,0xa6,0x06,0x06,0x04,0x2b,0xce,0x0f,0x0d,0xa7,0x03,0x02,0x01,0x01,0xbe,0x2f,0x28,0x2d,0x02,0x01,0x03,0xa0,0x28,0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 215 | 216 | 217 | 218 | //return []byte{0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 219 | } 220 | 221 | 222 | 223 | func newThreeUFrame(which byte) []byte { 224 | //return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 225 | //return []byte{0x03,0x00,0x00,0x16,0x11,0xe0,0x00,0x00,0x00,0x01,0x00,0xc0,0x01,0x0a,0xc2,0x02,0x00,0x01,0xc1,0x02,0x00,0x01} 226 | startMark = 0 227 | endMark = 47 228 | return []byte{0x03,0x00,0x00,0x24,0x02,0xf0,0x80,0x01,0x00,0x01,0x00,0x61,0x17,0x30,0x15,0x02,0x01,0x03,0xa0,0x10,0xa0,0x0e,0x02,0x01,0x01,0xa1,0x09,0xa0,0x03,0x80,0x01,0x09,0xa1,0x02,0x80,0x00} 229 | 230 | 231 | //return []byte{0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 232 | } 233 | 234 | //func newFourUFrame(which byte) []byte { 235 | //// 236 | //// //return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 237 | //// //return []byte{0x03,0x00,0x00,0x16,0x11,0xe0,0x00,0x00,0x00,0x01,0x00,0xc0,0x01,0x0a,0xc2,0x02,0x00,0x01,0xc1,0x02,0x00,0x01} 238 | //// test11 = 0 239 | //// test22 = 3000 240 | //// return []byte{0x03,0x00,0x00,0x2d,0x02,0xf0,0x80,0x01,0x00,0x01,0x00,0x61,0x20,0x30,0x1e,0x02,0x01,0x03,0xa0,0x19,0xa0,0x17,0x02,0x01,0x02,0xa1,0x12,0xa0,0x03,0x80,0x01,0x00,0xa1,0x0b,0x81,0x09,0x50,0x52,0x53,0x37,0x37,0x38,0x52,0x43,0x44} 241 | //// 242 | //// 243 | //// 244 | //// //return []byte{0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 245 | ////} 246 | 247 | func newFineUFrame() []byte { 248 | 249 | //return []byte{startFrame, 4, which | 0x03, 0x00, 0x00, 0x00} 250 | //return []byte{0x03,0x00,0x00,0x16,0x11,0xe0,0x00,0x00,0x00,0x01,0x00,0xc0,0x01,0x0a,0xc2,0x02,0x00,0x01,0xc1,0x02,0x00,0x01} 251 | startMark = 0 252 | endMark = 40 253 | return []byte{0x03,0x00,0x00,0x39,0x02,0xf0,0x80,0x01,0x00,0x01,0x00,0x61,0x2c,0x30,0x2a,0x02,0x01,0x03,0xa0,0x25,0xa0,0x23,0x02,0x01,0x11,0xa4,0x1e,0xa1,0x1c,0xa0,0x1a,0x30,0x18,0xa0,0x16,0xa1,0x14,0x1a,0x09,0x50,0x52,0x53,0x37,0x37,0x38,0x52,0x43,0x44,0x1a,0x07,0x4c,0x4c,0x4e,0x30,0x24,0x43,0x46} 254 | 255 | 256 | 257 | //return []byte{0xa8,0x26,0x80,0x03,0x00,0xfd,0xe8,0x81,0x01,0x05,0x82,0x01,0x05,0x83,0x01,0x0a,0xa4,0x16,0x80,0x01,0x01,0x81,0x03,0x05,0xf1,0x00,0x82,0x0c,0x03,0xee,0x1c,0x00,0x00,0x04,0x08,0x00,0x00,0x79,0xef,0x18} 258 | } 259 | 260 | 261 | 262 | 263 | 264 | 265 | // APCI apci 应用规约控制信息 266 | type APCI struct { 267 | start byte 268 | apduFiledLen byte // control + asdu 的长度 269 | ctr1, ctr2, ctr3, ctr4 byte 270 | } 271 | 272 | // return frame type , APCI, remain data 273 | func parse(apdu []byte) (interface{}, []byte) { 274 | apci := APCI{apdu[0], apdu[1], apdu[2], apdu[3], apdu[4], apdu[5]} 275 | if apci.ctr1&0x01 == 0 { 276 | return iAPCI{ 277 | sendSN: uint16(apci.ctr1)>>1 + uint16(apci.ctr2)<<7, 278 | rcvSN: uint16(apci.ctr3)>>1 + uint16(apci.ctr4)<<7, 279 | }, apdu[6:] 280 | } 281 | if apci.ctr1&0x03 == 0x01 { 282 | return sAPCI{ 283 | rcvSN: uint16(apci.ctr3)>>1 + uint16(apci.ctr4)<<7, 284 | }, apdu[6:] 285 | } 286 | // apci.ctrl&0x03 == 0x03 287 | return uAPCI{ 288 | function: apci.ctr1 & 0xfc, 289 | }, apdu[6:] 290 | } 291 | -------------------------------------------------------------------------------- /iec61850/apci_test.go: -------------------------------------------------------------------------------- 1 | package iec61850 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 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "fmt" 11 | "github.com/themeyic/go-iec61850/asdu" 12 | "github.com/themeyic/go-iec61850/clog" 13 | "io" 14 | "math/rand" 15 | "net" 16 | "strings" 17 | "sync" 18 | "sync/atomic" 19 | "time" 20 | ) 21 | 22 | const ( 23 | inactive = iota 24 | active 25 | ) 26 | 27 | 28 | // Client is an IEC61850 client 29 | type Client struct { 30 | option ClientOption 31 | conn net.Conn 32 | handler ClientHandlerInterface 33 | 34 | // channel 35 | rcvASDU chan []byte // for received asdu 36 | sendASDU chan []byte // for send asdu 37 | RcvRaw chan []byte // for recvLoop raw cs104 frame 38 | sendRaw chan []byte // for sendLoop raw cs104 frame 39 | 40 | // I帧的发送与接收序号 41 | seqNoSend uint16 // sequence number of next outbound I-frame 42 | ackNoSend uint16 // outbound sequence number yet to be confirmed 43 | seqNoRcv uint16 // sequence number of next inbound I-frame 44 | ackNoRcv uint16 // inbound sequence number yet to be confirmed 45 | 46 | // maps sendTime I-frames to their respective sequence number 47 | pending []seqPending 48 | 49 | startDtActiveSendSince atomic.Value // 当发送startDtActive时,等待确认回复的超时间隔 50 | stopDtActiveSendSince atomic.Value // 当发起stopDtActive时,等待确认回复的超时 51 | 52 | // 连接状态 53 | status uint32 54 | rwMux sync.RWMutex 55 | isActive uint32 56 | 57 | // 其他 58 | clog.Clog 59 | 60 | wg sync.WaitGroup 61 | ctx context.Context 62 | cancel context.CancelFunc 63 | closeCancel context.CancelFunc 64 | 65 | onConnect func(c *Client) 66 | onConnectionLost func(c *Client) 67 | } 68 | 69 | // NewClient returns an IEC61850 client,default config and default asdu.ParamsWide params 70 | func NewClient( o *ClientOption) *Client { 71 | return &Client{ 72 | option: *o, 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("iec61850 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 | var Transfer = make(chan byte) 155 | 156 | var startMark int 157 | var endMark int 158 | 159 | func (sf *Client) recvLoop() { 160 | sf.Debug("recvLoop started") 161 | defer func() { 162 | if r := recover(); r != nil{ 163 | fmt.Println("panic",r) 164 | } 165 | sf.cancel() 166 | sf.wg.Done() 167 | sf.Debug("recvLoop stopped") 168 | }() 169 | 170 | for { 171 | rawData := make([]byte, 40) 172 | for rdCnt, length := startMark, endMark; rdCnt < length; { 173 | byteCount, err := io.ReadFull(sf.conn, rawData[rdCnt:length]) 174 | if err != nil { 175 | // See: https://github.com/golang/go/issues/4373 176 | if err != io.EOF && err != io.ErrClosedPipe || 177 | strings.Contains(err.Error(), "use of closed network connection") { 178 | sf.Error("receive failed, %v", err) 179 | return 180 | } 181 | if e, ok := err.(net.Error); ok && !e.Temporary() { 182 | sf.Error("receive failed, %v", err) 183 | return 184 | } 185 | if rdCnt == 0 && err == io.EOF { 186 | sf.Error("remote connect closed, %v", err) 187 | return 188 | } 189 | } 190 | 191 | rdCnt += byteCount 192 | if rdCnt == 0 { 193 | continue 194 | } else if rdCnt == 1 { 195 | if rawData[0] != startFrame { 196 | rdCnt = 0 197 | continue 198 | } 199 | } else { 200 | continue 201 | } 202 | 203 | } 204 | sf.Debug("RX Raw[% x]", rawData) 205 | theIec61850Data := fmt.Sprintf("%x", rawData) 206 | if (strings.Contains(theIec61850Data, "0300001611")) { 207 | sf.sendRaw <- newSecondUFrame() 208 | } else if (strings.Contains(theIec61850Data, "a416800101810305f1")) { 209 | //sf.sendRaw <- newThreeUFrame(byte(1)) 210 | sf.sendRaw <- newFineUFrame() 211 | }else if(strings.Contains(theIec61850Data,"0103a010a10e020111a409a107a205a20385")){ 212 | //fmt.Println("所以这个",test) 213 | theValue := HexStringToBytes(theIec61850Data) 214 | Transfer <- theValue[39] 215 | }else{ 216 | Transfer <- 0 217 | } 218 | } 219 | } 220 | 221 | //把字符串转换成字节数组 222 | func HexStringToBytes(data string) []byte { 223 | if "" == data { 224 | return nil 225 | } 226 | data = strings.ToUpper(data) 227 | length := len(data) / 2 228 | dataChars := []byte(data) 229 | var byteData []byte = make([]byte, length) 230 | for i := 0; i < length; i++ { 231 | pos := i * 2 232 | byteData[i] = byte(charToByte(dataChars[pos])<<4 | charToByte(dataChars[pos+1])) 233 | } 234 | return byteData 235 | 236 | } 237 | 238 | func charToByte(c byte) byte { 239 | return (byte)(strings.Index("0123456789ABCDEF", string(c))) 240 | } 241 | 242 | func (sf *Client) sendLoop() { 243 | sf.Debug("sendLoop started") 244 | //defer func() { 245 | // sf.cancel() 246 | // sf.wg.Done() 247 | // sf.Debug("sendLoop stopped") 248 | //}() 249 | for { 250 | select { 251 | case <-sf.ctx.Done(): 252 | return 253 | case apdu := <-sf.sendRaw: 254 | sf.Debug("TX Raw[% x]", apdu) 255 | for wrCnt := 0; len(apdu) > wrCnt; { 256 | byteCount, err := sf.conn.Write(apdu[wrCnt:]) 257 | if err != nil { 258 | // See: https://github.com/golang/go/issues/4373 259 | if err != io.EOF && err != io.ErrClosedPipe || 260 | strings.Contains(err.Error(), "use of closed network connection") { 261 | sf.Error("sendRaw failed, %v", err) 262 | return 263 | } 264 | if e, ok := err.(net.Error); !ok || !e.Temporary() { 265 | sf.Error("sendRaw failed, %v", err) 266 | return 267 | } 268 | // temporary error may be recoverable 269 | } 270 | wrCnt += byteCount 271 | } 272 | } 273 | } 274 | } 275 | 276 | var MidData []byte 277 | 278 | // run is the big fat state machine. 279 | func (sf *Client) run(ctx context.Context) { 280 | 281 | sf.Debug("run started!") 282 | // before any thing make sure init 283 | sf.cleanUp() 284 | 285 | sf.ctx, sf.cancel = context.WithCancel(ctx) 286 | sf.setConnectStatus(connected) 287 | sf.wg.Add(3) 288 | go sf.recvLoop() 289 | go sf.sendLoop() 290 | //go sf.handlerLoop() 291 | 292 | var checkTicker = time.NewTicker(timeoutResolution) 293 | 294 | // transmission timestamps for timeout calculation 295 | var willNotTimeout = time.Now().Add(time.Hour * 24 * 365 * 100) 296 | 297 | var unAckRcvSince = willNotTimeout 298 | var idleTimeout3Sine = time.Now() // 空闲间隔发起testFrAlive 299 | var testFrAliveSendSince = willNotTimeout // 当发起testFrAlive时,等待确认回复的超时间隔 300 | 301 | sf.startDtActiveSendSince.Store(willNotTimeout) 302 | sf.stopDtActiveSendSince.Store(willNotTimeout) 303 | 304 | sendSFrame := func(rcvSN uint16) { 305 | sf.Debug("TX sFrame %v", sAPCI{rcvSN}) 306 | sf.sendRaw <- newSFrame(rcvSN) 307 | } 308 | 309 | sendIFrame := func(asdu1 []byte) { 310 | seqNo := sf.seqNoSend 311 | 312 | iframe, err := newIFrame(seqNo, sf.seqNoRcv, asdu1) 313 | if err != nil { 314 | return 315 | } 316 | sf.ackNoRcv = sf.seqNoRcv 317 | sf.seqNoSend = (seqNo + 1) & 32767 318 | sf.pending = append(sf.pending, seqPending{seqNo & 32767, time.Now()}) 319 | 320 | sf.Debug("TX iFrame %v", iAPCI{seqNo, sf.seqNoRcv}) 321 | sf.sendRaw <- iframe 322 | } 323 | 324 | defer func() { 325 | // default: STOPDT, when connected establish and not enable "data transfer" yet 326 | atomic.StoreUint32(&sf.isActive, inactive) 327 | sf.setConnectStatus(disconnected) 328 | checkTicker.Stop() 329 | _ = sf.conn.Close() // 连锁引发cancel 330 | sf.wg.Wait() 331 | sf.onConnectionLost(sf) 332 | sf.Debug("run stopped!") 333 | }() 334 | 335 | sf.onConnect(sf) 336 | for { 337 | if atomic.LoadUint32(&sf.isActive) == active && seqNoCount(sf.ackNoSend, sf.seqNoSend) <= sf.option.config.SendUnAckLimitK { 338 | select { 339 | case o := <-sf.sendASDU: 340 | sendIFrame(o) 341 | idleTimeout3Sine = time.Now() 342 | continue 343 | case <-sf.ctx.Done(): 344 | return 345 | default: // make no block 346 | } 347 | } 348 | select { 349 | case <-sf.ctx.Done(): 350 | return 351 | case now := <-checkTicker.C: 352 | // check all timeouts 353 | if now.Sub(testFrAliveSendSince) >= sf.option.config.SendUnAckTimeout1 || 354 | now.Sub(sf.startDtActiveSendSince.Load().(time.Time)) >= sf.option.config.SendUnAckTimeout1 || 355 | now.Sub(sf.stopDtActiveSendSince.Load().(time.Time)) >= sf.option.config.SendUnAckTimeout1 { 356 | sf.Error("test frame alive confirm timeout t₁") 357 | return 358 | } 359 | // check oldest unacknowledged outbound 360 | if sf.ackNoSend != sf.seqNoSend && 361 | //now.Sub(sf.peek()) >= sf.SendUnAckTimeout1 { 362 | now.Sub(sf.pending[0].sendTime) >= sf.option.config.SendUnAckTimeout1 { 363 | sf.ackNoSend++ 364 | sf.Error("fatal transmission timeout t₁") 365 | return 366 | } 367 | 368 | // 确定最早发送的i-Frame是否超时,超时则回复sFrame 369 | if sf.ackNoRcv != sf.seqNoRcv && 370 | (now.Sub(unAckRcvSince) >= sf.option.config.RecvUnAckTimeout2 || 371 | now.Sub(idleTimeout3Sine) >= timeoutResolution) { 372 | sendSFrame(sf.seqNoRcv) 373 | sf.ackNoRcv = sf.seqNoRcv 374 | } 375 | 376 | // 空闲时间到,发送TestFrActive帧,保活 377 | //if now.Sub(idleTimeout3Sine) >= sf.option.config.IdleTimeout3 { 378 | // sf.sendUFrame(uTestFrActive) 379 | // testFrAliveSendSince = time.Now() 380 | // idleTimeout3Sine = testFrAliveSendSince 381 | //} 382 | case apdu := <-sf.RcvRaw: 383 | fmt.Println("?",apdu) 384 | } 385 | } 386 | } 387 | 388 | func (sf *Client) handlerLoop() { 389 | sf.Debug("handlerLoop started") 390 | defer func() { 391 | sf.wg.Done() 392 | sf.Debug("handlerLoop stopped") 393 | }() 394 | 395 | for { 396 | select { 397 | case <-sf.ctx.Done(): 398 | return 399 | case rawAsdu := <-sf.rcvASDU: 400 | asduPack := asdu.NewEmptyASDU(&sf.option.params) 401 | if err := asduPack.UnmarshalBinary(rawAsdu); err != nil { 402 | sf.Warn("asdu UnmarshalBinary failed,%+v", err) 403 | continue 404 | } 405 | if err := sf.clientHandler(asduPack); err != nil { 406 | sf.Warn("Falied handling I frame, error: %v", err) 407 | } 408 | } 409 | } 410 | } 411 | 412 | func (sf *Client) setConnectStatus(status uint32) { 413 | sf.rwMux.Lock() 414 | atomic.StoreUint32(&sf.status, status) 415 | sf.rwMux.Unlock() 416 | } 417 | 418 | func (sf *Client) connectStatus() uint32 { 419 | sf.rwMux.RLock() 420 | status := atomic.LoadUint32(&sf.status) 421 | sf.rwMux.RUnlock() 422 | return status 423 | } 424 | 425 | func (sf *Client) cleanUp() { 426 | sf.ackNoRcv = 0 427 | sf.ackNoSend = 0 428 | sf.seqNoRcv = 0 429 | sf.seqNoSend = 0 430 | sf.pending = nil 431 | // clear sending chan buffer 432 | loop: 433 | for { 434 | select { 435 | case <-sf.sendRaw: 436 | case <-sf.RcvRaw: 437 | case <-sf.rcvASDU: 438 | case <-sf.sendASDU: 439 | default: 440 | break loop 441 | } 442 | } 443 | } 444 | 445 | func (sf *Client) sendInitFrame() { 446 | sf.sendRaw <- newInitFrame() 447 | } 448 | 449 | func (sf *Client) sendSecondUFrame() { 450 | sf.sendRaw <- newSecondUFrame() 451 | } 452 | 453 | 454 | 455 | func (sf *Client) updateAckNoOut(ackNo uint16) (ok bool) { 456 | if ackNo == sf.ackNoSend { 457 | return true 458 | } 459 | // new acks validate, ack 不能在 req seq 前面,出错 460 | if seqNoCount(sf.ackNoSend, sf.seqNoSend) < seqNoCount(ackNo, sf.seqNoSend) { 461 | return false 462 | } 463 | 464 | // confirm reception 465 | for i, v := range sf.pending { 466 | if v.seq == (ackNo - 1) { 467 | sf.pending = sf.pending[i+1:] 468 | break 469 | } 470 | } 471 | 472 | sf.ackNoSend = ackNo 473 | return true 474 | } 475 | 476 | // IsConnected get server session connected state 477 | func (sf *Client) IsConnected() bool { 478 | return sf.connectStatus() == connected 479 | } 480 | 481 | // clientHandler hand response handler 482 | func (sf *Client) clientHandler(asduPack *asdu.ASDU) error { 483 | defer func() { 484 | if err := recover(); err != nil { 485 | sf.Critical("client handler %+v", err) 486 | } 487 | }() 488 | 489 | sf.Debug("ASDU %+v", asduPack) 490 | 491 | switch asduPack.Identifier.Type { 492 | case asdu.C_IC_NA_1: // InterrogationCmd 493 | return sf.handler.InterrogationHandler(sf, asduPack) 494 | 495 | case asdu.C_CI_NA_1: // CounterInterrogationCmd 496 | return sf.handler.CounterInterrogationHandler(sf, asduPack) 497 | 498 | case asdu.C_RD_NA_1: // ReadCmd 499 | return sf.handler.ReadHandler(sf, asduPack) 500 | 501 | case asdu.C_CS_NA_1: // ClockSynchronizationCmd 502 | return sf.handler.ClockSyncHandler(sf, asduPack) 503 | 504 | case asdu.C_TS_NA_1: // TestCommand 505 | return sf.handler.TestCommandHandler(sf, asduPack) 506 | 507 | case asdu.C_RP_NA_1: // ResetProcessCmd 508 | return sf.handler.ResetProcessHandler(sf, asduPack) 509 | 510 | case asdu.C_CD_NA_1: // DelayAcquireCommand 511 | return sf.handler.DelayAcquisitionHandler(sf, asduPack) 512 | } 513 | 514 | return sf.handler.ASDUHandler(sf, asduPack) 515 | } 516 | 517 | // Params returns params of client 518 | func (sf *Client) Params() *asdu.Params { 519 | return &sf.option.params 520 | } 521 | 522 | // Send send asdu 523 | func (sf *Client) Send(a *asdu.ASDU) error { 524 | if !sf.IsConnected() { 525 | return ErrUseClosedConnection 526 | } 527 | if atomic.LoadUint32(&sf.isActive) == inactive { 528 | return ErrNotActive 529 | } 530 | data, err := a.MarshalBinary() 531 | if err != nil { 532 | return err 533 | } 534 | select { 535 | case sf.sendASDU <- data: 536 | default: 537 | return ErrBufferFulled 538 | } 539 | return nil 540 | } 541 | 542 | // UnderlyingConn returns underlying conn of client 543 | func (sf *Client) UnderlyingConn() net.Conn { 544 | return sf.conn 545 | } 546 | 547 | // Close close all 548 | func (sf *Client) Close() error { 549 | sf.rwMux.Lock() 550 | if sf.closeCancel != nil { 551 | sf.closeCancel() 552 | } 553 | sf.rwMux.Unlock() 554 | return nil 555 | } 556 | 557 | // SendStartDt start data transmission on this connection 558 | func (sf *Client) SendStartDt() { 559 | sf.startDtActiveSendSince.Store(time.Now()) 560 | sf.sendInitFrame() 561 | } 562 | 563 | //Second 564 | func (sf *Client) SendSecondStartDt() { 565 | sf.startDtActiveSendSince.Store(time.Now()) 566 | sf.sendSecondUFrame() 567 | } 568 | 569 | 570 | // SendStopDt stop data transmission on this connection 571 | func (sf *Client) SendStopDt() { 572 | sf.stopDtActiveSendSince.Store(time.Now()) 573 | sf.sendInitFrame() 574 | } 575 | 576 | //InterrogationCmd wrap asdu.InterrogationCmd 577 | func (sf *Client) InterrogationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qoi asdu.QualifierOfInterrogation) error { 578 | return asdu.InterrogationCmd(sf, coa, ca, qoi) 579 | } 580 | 581 | // CounterInterrogationCmd wrap asdu.CounterInterrogationCmd 582 | func (sf *Client) CounterInterrogationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qcc asdu.QualifierCountCall) error { 583 | return asdu.CounterInterrogationCmd(sf, coa, ca, qcc) 584 | } 585 | 586 | // ReadCmd wrap asdu.ReadCmd 587 | func (sf *Client) ReadCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, ioa asdu.InfoObjAddr) error { 588 | return asdu.ReadCmd(sf, coa, ca, ioa) 589 | } 590 | 591 | // ClockSynchronizationCmd wrap asdu.ClockSynchronizationCmd 592 | func (sf *Client) ClockSynchronizationCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, t time.Time) error { 593 | return asdu.ClockSynchronizationCmd(sf, coa, ca, t) 594 | } 595 | 596 | // ResetProcessCmd wrap asdu.ResetProcessCmd 597 | func (sf *Client) ResetProcessCmd(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, qrp asdu.QualifierOfResetProcessCmd) error { 598 | return asdu.ResetProcessCmd(sf, coa, ca, qrp) 599 | } 600 | 601 | // DelayAcquireCommand wrap asdu.DelayAcquireCommand 602 | func (sf *Client) DelayAcquireCommand(coa asdu.CauseOfTransmission, ca asdu.CommonAddr, msec uint16) error { 603 | return asdu.DelayAcquireCommand(sf, coa, ca, msec) 604 | } 605 | 606 | // TestCommand wrap asdu.TestCommand 607 | func (sf *Client) TestCommand(coa asdu.CauseOfTransmission, ca asdu.CommonAddr) error { 608 | return asdu.TestCommand(sf, coa, ca) 609 | } 610 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "crypto/tls" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/themeyic/go-iec61850/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 | //server = "mms://" + server 89 | } 90 | remoteURL, err := url.Parse(server) 91 | if err != nil { 92 | return err 93 | } 94 | sf.server = remoteURL 95 | return nil 96 | } 97 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 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 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 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 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 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 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "time" 9 | 10 | "github.com/themeyic/go-iec61850/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 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "context" 9 | "crypto/tls" 10 | "net" 11 | "sync" 12 | "time" 13 | 14 | "github.com/themeyic/go-iec61850/asdu" 15 | "github.com/themeyic/go-iec61850/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 | clog.Clog 33 | wg sync.WaitGroup 34 | } 35 | 36 | // NewServer new a server, default config and default asdu.ParamsWide params 37 | func NewServer(handler ServerHandlerInterface) *Server { 38 | return &Server{ 39 | config: DefaultConfig(), 40 | params: *asdu.ParamsWide, 41 | handler: handler, 42 | sessions: make(map[*SrvSession]struct{}), 43 | Clog: clog.NewLogger("cs104 server => "), 44 | } 45 | } 46 | 47 | // SetConfig set config if config is valid it will use DefaultConfig() 48 | func (sf *Server) SetConfig(cfg Config) *Server { 49 | if err := cfg.Valid(); err != nil { 50 | sf.config = DefaultConfig() 51 | } else { 52 | sf.config = cfg 53 | } 54 | return sf 55 | } 56 | 57 | // SetParams set asdu params if params is valid it will use asdu.ParamsWide 58 | func (sf *Server) SetParams(p *asdu.Params) *Server { 59 | if err := p.Valid(); err != nil { 60 | sf.params = *asdu.ParamsWide 61 | } else { 62 | sf.params = *p 63 | } 64 | return sf 65 | } 66 | 67 | // ListenAndServer run the server 68 | func (sf *Server) ListenAndServer(addr string) { 69 | listen, err := net.Listen("tcp", addr) 70 | if err != nil { 71 | sf.Error("server run failed, %v", err) 72 | return 73 | } 74 | sf.mux.Lock() 75 | sf.listen = listen 76 | sf.mux.Unlock() 77 | 78 | ctx, cancel := context.WithCancel(context.Background()) 79 | defer func() { 80 | cancel() 81 | _ = sf.Close() 82 | sf.Debug("server stop") 83 | }() 84 | sf.Debug("server run") 85 | for { 86 | conn, err := listen.Accept() 87 | if err != nil { 88 | sf.Error("server run failed, %v", err) 89 | return 90 | } 91 | 92 | sf.wg.Add(1) 93 | go func() { 94 | sess := &SrvSession{ 95 | config: &sf.config, 96 | params: &sf.params, 97 | handler: sf.handler, 98 | conn: conn, 99 | rcvASDU: make(chan []byte, sf.config.RecvUnAckLimitW<<4), 100 | sendASDU: make(chan []byte, sf.config.SendUnAckLimitK<<4), 101 | rcvRaw: make(chan []byte, sf.config.RecvUnAckLimitW<<5), 102 | sendRaw: make(chan []byte, sf.config.SendUnAckLimitK<<5), // may not block! 103 | 104 | Clog: sf.Clog, 105 | } 106 | sf.mux.Lock() 107 | sf.sessions[sess] = struct{}{} 108 | sf.mux.Unlock() 109 | sess.run(ctx) 110 | sf.mux.Lock() 111 | delete(sf.sessions, sess) 112 | sf.mux.Unlock() 113 | sf.wg.Done() 114 | }() 115 | } 116 | } 117 | 118 | // Close close the server 119 | func (sf *Server) Close() error { 120 | var err error 121 | 122 | sf.mux.Lock() 123 | if sf.listen != nil { 124 | err = sf.listen.Close() 125 | sf.listen = nil 126 | } 127 | sf.mux.Unlock() 128 | sf.wg.Wait() 129 | return err 130 | } 131 | 132 | // Send imp interface Connect 133 | func (sf *Server) Send(a *asdu.ASDU) error { 134 | sf.mux.Lock() 135 | for k := range sf.sessions { 136 | _ = k.Send(a.Clone()) 137 | } 138 | sf.mux.Unlock() 139 | return nil 140 | } 141 | 142 | // Params imp interface Connect 143 | func (sf *Server) Params() *asdu.Params { return &sf.params } 144 | 145 | // UnderlyingConn imp interface Connect 146 | func (sf *Server) UnderlyingConn() net.Conn { return nil } 147 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "context" 9 | "io" 10 | "net" 11 | "strings" 12 | "sync" 13 | "sync/atomic" 14 | "time" 15 | 16 | "github.com/themeyic/go-iec61850/asdu" 17 | "github.com/themeyic/go-iec61850/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 | wg sync.WaitGroup 53 | cancel context.CancelFunc 54 | ctx context.Context 55 | } 56 | 57 | // RecvLoop feeds t.rcvRaw. 58 | func (sf *SrvSession) recvLoop() { 59 | sf.Debug("recvLoop started!") 60 | defer func() { 61 | sf.cancel() 62 | sf.wg.Done() 63 | sf.Debug("recvLoop stopped!") 64 | }() 65 | 66 | for { 67 | rawData := make([]byte, APDUSizeMax) 68 | for rdCnt, length := 0, 2; rdCnt < length; { 69 | byteCount, err := io.ReadFull(sf.conn, rawData[rdCnt:length]) 70 | if err != nil { 71 | // See: https://github.com/golang/go/issues/4373 72 | if err != io.EOF && err != io.ErrClosedPipe || 73 | strings.Contains(err.Error(), "use of closed network connection") { 74 | sf.Error("receive failed, %v", err) 75 | return 76 | } 77 | if e, ok := err.(net.Error); ok && !e.Temporary() { 78 | sf.Error("receive failed, %v", err) 79 | return 80 | } 81 | 82 | if byteCount == 0 && err == io.EOF { 83 | sf.Error("remote connect closed, %v", err) 84 | return 85 | } 86 | } 87 | 88 | rdCnt += byteCount 89 | if rdCnt == 0 { 90 | continue 91 | } else if rdCnt == 1 { 92 | if rawData[0] != startFrame { 93 | rdCnt = 0 94 | continue 95 | } 96 | } else { 97 | if rawData[0] != startFrame { 98 | rdCnt, length = 0, 2 99 | continue 100 | } 101 | length = int(rawData[1]) + 2 102 | if length < APCICtlFiledSize+2 || length > APDUSizeMax { 103 | rdCnt, length = 0, 2 104 | continue 105 | } 106 | if rdCnt == length { 107 | apdu := rawData[:length] 108 | sf.Debug("RX Raw[% x]", apdu) 109 | sf.rcvRaw <- apdu 110 | } 111 | } 112 | } 113 | } 114 | } 115 | 116 | // sendLoop drains t.sendTime. 117 | func (sf *SrvSession) sendLoop() { 118 | sf.Debug("sendLoop started!") 119 | defer func() { 120 | sf.cancel() 121 | sf.wg.Done() 122 | sf.Debug("sendLoop stopped!") 123 | }() 124 | 125 | for { 126 | select { 127 | case <-sf.ctx.Done(): 128 | return 129 | case apdu := <-sf.sendRaw: 130 | sf.Debug("TX Raw[% x]", apdu) 131 | for wrCnt := 0; len(apdu) > wrCnt; { 132 | byteCount, err := sf.conn.Write(apdu[wrCnt:]) 133 | if err != nil { 134 | // See: https://github.com/golang/go/issues/4373 135 | if err != io.EOF && err != io.ErrClosedPipe || 136 | strings.Contains(err.Error(), "use of closed network connection") { 137 | sf.Error("sendRaw failed, %v", err) 138 | return 139 | } 140 | if e, ok := err.(net.Error); !ok || !e.Temporary() { 141 | sf.Error("sendRaw failed, %v", err) 142 | return 143 | } 144 | // temporary error may be recoverable 145 | } 146 | wrCnt += byteCount 147 | } 148 | } 149 | } 150 | } 151 | 152 | // run is the big fat state machine. 153 | func (sf *SrvSession) run(ctx context.Context) { 154 | sf.Debug("run started!") 155 | // before any thing make sure init 156 | sf.cleanUp() 157 | 158 | sf.ctx, sf.cancel = context.WithCancel(ctx) 159 | sf.setConnectStatus(connected) 160 | sf.wg.Add(3) 161 | go sf.recvLoop() 162 | go sf.sendLoop() 163 | go sf.handlerLoop() 164 | 165 | // default: STOPDT, when connected establish and not enable "data transfer" yet 166 | var isActive = false 167 | var checkTicker = time.NewTicker(timeoutResolution) 168 | 169 | // transmission timestamps for timeout calculation 170 | var willNotTimeout = time.Now().Add(time.Hour * 24 * 365 * 100) 171 | 172 | var unAckRcvSince = willNotTimeout 173 | var idleTimeout3Sine = time.Now() // 空闲间隔发起testFrAlive 174 | var testFrAliveSendSince = willNotTimeout // 当发起testFrAlive时,等待确认回复的超时间隔 175 | // 对于server端,无需对应的U-Frame 无需判断 176 | // var startDtActiveSendSince = willNotTimeout 177 | // var stopDtActiveSendSince = willNotTimeout 178 | 179 | sendSFrame := func(rcvSN uint16) { 180 | sf.Debug("TX sFrame %v", sAPCI{rcvSN}) 181 | sf.sendRaw <- newSFrame(rcvSN) 182 | } 183 | sendUFrame := func(which byte) { 184 | sf.Debug("TX uFrame %v", uAPCI{which}) 185 | sf.sendRaw <- newInitFrame() 186 | } 187 | 188 | sendIFrame := func(asdu1 []byte) { 189 | seqNo := sf.seqNoSend 190 | 191 | iframe, err := newIFrame(seqNo, sf.seqNoRcv, asdu1) 192 | if err != nil { 193 | return 194 | } 195 | sf.ackNoRcv = sf.seqNoRcv 196 | sf.seqNoSend = (seqNo + 1) & 32767 197 | sf.pending = append(sf.pending, seqPending{seqNo & 32767, time.Now()}) 198 | 199 | sf.Debug("TX iFrame %v", iAPCI{seqNo, sf.seqNoRcv}) 200 | sf.sendRaw <- iframe 201 | } 202 | 203 | defer func() { 204 | sf.setConnectStatus(disconnected) 205 | checkTicker.Stop() 206 | _ = sf.conn.Close() // 连锁引发cancel 207 | sf.wg.Wait() 208 | sf.Debug("run stopped!") 209 | }() 210 | 211 | for { 212 | if isActive && seqNoCount(sf.ackNoSend, sf.seqNoSend) <= sf.config.SendUnAckLimitK { 213 | select { 214 | case o := <-sf.sendASDU: 215 | sendIFrame(o) 216 | idleTimeout3Sine = time.Now() 217 | continue 218 | case <-sf.ctx.Done(): 219 | return 220 | default: // make no block 221 | } 222 | } 223 | select { 224 | case <-sf.ctx.Done(): 225 | return 226 | case now := <-checkTicker.C: 227 | // check all timeouts 228 | if now.Sub(testFrAliveSendSince) >= sf.config.SendUnAckTimeout1 { 229 | // now.Sub(startDtActiveSendSince) >= t.SendUnAckTimeout1 || 230 | // now.Sub(stopDtActiveSendSince) >= t.SendUnAckTimeout1 || 231 | sf.Error("finished") 232 | return 233 | } 234 | // check oldest unacknowledged outbound 235 | if sf.ackNoSend != sf.seqNoSend && 236 | //now.Sub(sf.peek()) >= sf.SendUnAckTimeout1 { 237 | now.Sub(sf.pending[0].sendTime) >= sf.config.SendUnAckTimeout1 { 238 | sf.ackNoSend++ 239 | sf.Error("finished") 240 | return 241 | } 242 | 243 | // 确定最早发送的i-Frame是否超时,超时则回复sFrame 244 | if sf.ackNoRcv != sf.seqNoRcv && 245 | (now.Sub(unAckRcvSince) >= sf.config.RecvUnAckTimeout2 || 246 | now.Sub(idleTimeout3Sine) >= timeoutResolution) { 247 | sendSFrame(sf.seqNoRcv) 248 | sf.ackNoRcv = sf.seqNoRcv 249 | } 250 | 251 | // 空闲时间到,发送TestFrActive帧,保活 252 | if now.Sub(idleTimeout3Sine) >= sf.config.IdleTimeout3 { 253 | sendUFrame(uTestFrActive) 254 | testFrAliveSendSince = time.Now() 255 | idleTimeout3Sine = testFrAliveSendSince 256 | } 257 | 258 | case apdu := <-sf.rcvRaw: 259 | idleTimeout3Sine = time.Now() // 每收到一个i帧,S帧,U帧, 重置空闲定时器, t3 260 | apci, asduVal := parse(apdu) 261 | switch head := apci.(type) { 262 | case sAPCI: 263 | sf.Debug("RX sFrame %v", head) 264 | if !sf.updateAckNoOut(head.rcvSN) { 265 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 266 | return 267 | } 268 | 269 | case iAPCI: 270 | sf.Debug("RX iFrame %v", head) 271 | if !isActive { 272 | sf.Warn("station not active") 273 | break // not active, discard apdu 274 | } 275 | if !sf.updateAckNoOut(head.rcvSN) || head.sendSN != sf.seqNoRcv { 276 | sf.Error("fatal incoming acknowledge either earlier than previous or later than sendTime") 277 | return 278 | } 279 | 280 | sf.rcvASDU <- asduVal 281 | if sf.ackNoRcv == sf.seqNoRcv { // first unacked 282 | unAckRcvSince = time.Now() 283 | } 284 | 285 | sf.seqNoRcv = (sf.seqNoRcv + 1) & 32767 286 | if seqNoCount(sf.ackNoRcv, sf.seqNoRcv) >= sf.config.RecvUnAckLimitW { 287 | sendSFrame(sf.seqNoRcv) 288 | sf.ackNoRcv = sf.seqNoRcv 289 | } 290 | 291 | case uAPCI: 292 | sf.Debug("RX uFrame %v", head) 293 | switch head.function { 294 | case uStartDtActive: 295 | sendUFrame(uStartDtConfirm) 296 | isActive = true 297 | // case uStartDtConfirm: 298 | // isActive = true 299 | // startDtActiveSendSince = willNotTimeout 300 | case uStopDtActive: 301 | sendUFrame(uStopDtConfirm) 302 | isActive = false 303 | // case uStopDtConfirm: 304 | // isActive = false 305 | // stopDtActiveSendSince = willNotTimeout 306 | case uTestFrActive: 307 | sendUFrame(uTestFrConfirm) 308 | case uTestFrConfirm: 309 | testFrAliveSendSince = willNotTimeout 310 | default: 311 | sf.Error("illegal U-Frame functions[0x%02x] ignored", head.function) 312 | } 313 | } 314 | } 315 | } 316 | } 317 | 318 | // handlerLoop handler iFrame asdu 319 | func (sf *SrvSession) handlerLoop() { 320 | sf.Debug("handlerLoop started") 321 | defer func() { 322 | sf.wg.Done() 323 | sf.Debug("handlerLoop stopped") 324 | }() 325 | 326 | for { 327 | select { 328 | case <-sf.ctx.Done(): 329 | return 330 | case rawAsdu := <-sf.rcvASDU: 331 | asduPack := asdu.NewEmptyASDU(sf.params) 332 | if err := asduPack.UnmarshalBinary(rawAsdu); err != nil { 333 | sf.Error("asdu UnmarshalBinary failed,%+v", err) 334 | continue 335 | } 336 | if err := sf.serverHandler(asduPack); err != nil { 337 | sf.Error("serverHandler falied,%+v", err) 338 | } 339 | } 340 | } 341 | } 342 | 343 | func (sf *SrvSession) setConnectStatus(status uint32) { 344 | sf.rwMux.Lock() 345 | atomic.StoreUint32(&sf.status, status) 346 | sf.rwMux.Unlock() 347 | } 348 | 349 | func (sf *SrvSession) connectStatus() uint32 { 350 | sf.rwMux.RLock() 351 | status := atomic.LoadUint32(&sf.status) 352 | sf.rwMux.RUnlock() 353 | return status 354 | } 355 | 356 | func (sf *SrvSession) cleanUp() { 357 | sf.ackNoRcv = 0 358 | sf.ackNoSend = 0 359 | sf.seqNoRcv = 0 360 | sf.seqNoSend = 0 361 | sf.pending = nil 362 | // clear sending chan buffer 363 | loop: 364 | for { 365 | select { 366 | case <-sf.sendRaw: 367 | case <-sf.rcvRaw: 368 | case <-sf.rcvASDU: 369 | case <-sf.sendASDU: 370 | default: 371 | break loop 372 | } 373 | } 374 | } 375 | 376 | // 回绕机制 377 | func seqNoCount(nextAckNo, nextSeqNo uint16) uint16 { 378 | if nextAckNo > nextSeqNo { 379 | nextSeqNo += 32768 380 | } 381 | return nextSeqNo - nextAckNo 382 | } 383 | 384 | func (sf *SrvSession) updateAckNoOut(ackNo uint16) (ok bool) { 385 | if ackNo == sf.ackNoSend { 386 | return true 387 | } 388 | // new acks validate, ack 不能在 req seq 前面,出错 389 | if seqNoCount(sf.ackNoSend, sf.seqNoSend) < seqNoCount(ackNo, sf.seqNoSend) { 390 | return false 391 | } 392 | 393 | // confirm reception 394 | for i, v := range sf.pending { 395 | if v.seq == (ackNo - 1) { 396 | sf.pending = sf.pending[i+1:] 397 | break 398 | } 399 | } 400 | 401 | sf.ackNoSend = ackNo 402 | return true 403 | } 404 | 405 | func (sf *SrvSession) serverHandler(asduPack *asdu.ASDU) error { 406 | defer func() { 407 | if err := recover(); err != nil { 408 | sf.Critical("server handler %+v", err) 409 | } 410 | }() 411 | 412 | sf.Debug("ASDU %+v", asduPack) 413 | 414 | switch asduPack.Identifier.Type { 415 | case asdu.C_IC_NA_1: // InterrogationCmd 416 | if !(asduPack.Identifier.Coa.Cause == asdu.Activation || 417 | asduPack.Identifier.Coa.Cause == asdu.Deactivation) { 418 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 419 | } 420 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 421 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 422 | } 423 | ioa, qoi := asduPack.GetInterrogationCmd() 424 | if ioa != asdu.InfoObjAddrIrrelevant { 425 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 426 | } 427 | return sf.handler.InterrogationHandler(sf, asduPack, qoi) 428 | 429 | case asdu.C_CI_NA_1: // CounterInterrogationCmd 430 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 431 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 432 | } 433 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 434 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 435 | } 436 | ioa, qcc := asduPack.GetCounterInterrogationCmd() 437 | if ioa != asdu.InfoObjAddrIrrelevant { 438 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 439 | } 440 | return sf.handler.CounterInterrogationHandler(sf, asduPack, qcc) 441 | 442 | case asdu.C_RD_NA_1: // ReadCmd 443 | if asduPack.Identifier.Coa.Cause != asdu.Request { 444 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 445 | } 446 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 447 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 448 | } 449 | return sf.handler.ReadHandler(sf, asduPack, asduPack.GetReadCmd()) 450 | 451 | case asdu.C_CS_NA_1: // ClockSynchronizationCmd 452 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 453 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 454 | } 455 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 456 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 457 | } 458 | 459 | ioa, tm := asduPack.GetClockSynchronizationCmd() 460 | if ioa != asdu.InfoObjAddrIrrelevant { 461 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 462 | } 463 | return sf.handler.ClockSyncHandler(sf, asduPack, tm) 464 | 465 | case asdu.C_TS_NA_1: // TestCommand 466 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 467 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 468 | } 469 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 470 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 471 | } 472 | ioa, _ := asduPack.GetTestCommand() 473 | if ioa != asdu.InfoObjAddrIrrelevant { 474 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 475 | } 476 | return asduPack.SendReplyMirror(sf, asdu.ActivationCon) 477 | 478 | case asdu.C_RP_NA_1: // ResetProcessCmd 479 | if asduPack.Identifier.Coa.Cause != asdu.Activation { 480 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 481 | } 482 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 483 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 484 | } 485 | ioa, qrp := asduPack.GetResetProcessCmd() 486 | if ioa != asdu.InfoObjAddrIrrelevant { 487 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 488 | } 489 | return sf.handler.ResetProcessHandler(sf, asduPack, qrp) 490 | case asdu.C_CD_NA_1: // DelayAcquireCommand 491 | if !(asduPack.Identifier.Coa.Cause == asdu.Activation || 492 | asduPack.Identifier.Coa.Cause == asdu.Spontaneous) { 493 | return asduPack.SendReplyMirror(sf, asdu.UnknownCOT) 494 | } 495 | if asduPack.CommonAddr == asdu.InvalidCommonAddr { 496 | return asduPack.SendReplyMirror(sf, asdu.UnknownCA) 497 | } 498 | ioa, msec := asduPack.GetDelayAcquireCommand() 499 | if ioa != asdu.InfoObjAddrIrrelevant { 500 | return asduPack.SendReplyMirror(sf, asdu.UnknownIOA) 501 | } 502 | return sf.handler.DelayAcquisitionHandler(sf, asduPack, msec) 503 | } 504 | 505 | if err := sf.handler.ASDUHandler(sf, asduPack); err != nil { 506 | return asduPack.SendReplyMirror(sf, asdu.UnknownTypeID) 507 | } 508 | return nil 509 | } 510 | 511 | // IsConnected get server session connected state 512 | func (sf *SrvSession) IsConnected() bool { 513 | return sf.connectStatus() == connected 514 | } 515 | 516 | // Params get params 517 | func (sf *SrvSession) Params() *asdu.Params { 518 | return sf.params 519 | } 520 | 521 | // Send asdu frame 522 | func (sf *SrvSession) Send(u *asdu.ASDU) error { 523 | if !sf.IsConnected() { 524 | return ErrUseClosedConnection 525 | } 526 | data, err := u.MarshalBinary() 527 | if err != nil { 528 | return err 529 | } 530 | select { 531 | case sf.sendASDU <- data: 532 | default: 533 | return ErrBufferFulled 534 | } 535 | return nil 536 | } 537 | 538 | // UnderlyingConn got under net.conn 539 | func (sf *SrvSession) UnderlyingConn() net.Conn { 540 | return sf.conn 541 | } 542 | -------------------------------------------------------------------------------- /iec61850/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 iec61850 6 | 7 | import ( 8 | "context" 9 | "errors" 10 | "math/rand" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/themeyic/go-iec61850/asdu" 15 | "github.com/themeyic/go-iec61850/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 ServerSpecial)) ServerSpecial 28 | SetConnectionLostHandler(f func(c ServerSpecial)) ServerSpecial 29 | 30 | LogMode(enable bool) 31 | SetLogProvider(p clog.LogProvider) 32 | } 33 | 34 | type serverSpec struct { 35 | SrvSession 36 | option ClientOption 37 | onConnect func(c ServerSpecial) 38 | onConnectionLost func(c ServerSpecial) 39 | closeCancel context.CancelFunc 40 | } 41 | 42 | // NewServerSpecial new special server 43 | func NewServerSpecial(handler ServerHandlerInterface, o *ClientOption) ServerSpecial { 44 | return &serverSpec{ 45 | SrvSession: SrvSession{ 46 | config: &o.config, 47 | params: &o.params, 48 | handler: handler, 49 | 50 | rcvASDU: make(chan []byte, 1024), 51 | sendASDU: make(chan []byte, 1024), 52 | rcvRaw: make(chan []byte, 1024), 53 | sendRaw: make(chan []byte, 1024), // may not block! 54 | 55 | Clog: clog.NewLogger("cs104 serverSpec => "), 56 | }, 57 | option: *o, 58 | onConnect: func(ServerSpecial) {}, 59 | onConnectionLost: func(ServerSpecial) {}, 60 | } 61 | } 62 | 63 | // SetOnConnectHandler set on connect handler 64 | func (sf *serverSpec) SetOnConnectHandler(f func(conn ServerSpecial)) ServerSpecial { 65 | if f != nil { 66 | sf.onConnect = f 67 | } 68 | return sf 69 | } 70 | 71 | // SetConnectionLostHandler set connection lost handler 72 | func (sf *serverSpec) SetConnectionLostHandler(f func(c ServerSpecial)) ServerSpecial { 73 | if f != nil { 74 | sf.onConnectionLost = f 75 | } 76 | return sf 77 | } 78 | 79 | // Start start the server,and return quickly,if it nil,the server will disconnected background,other failed 80 | func (sf *serverSpec) Start() error { 81 | if sf.option.server == nil { 82 | return errors.New("empty remote server") 83 | } 84 | 85 | go sf.running() 86 | return nil 87 | } 88 | 89 | // 增加重连间隔 90 | func (sf *serverSpec) running() { 91 | var ctx context.Context 92 | 93 | sf.rwMux.Lock() 94 | if !atomic.CompareAndSwapUint32(&sf.status, initial, disconnected) { 95 | sf.rwMux.Unlock() 96 | return 97 | } 98 | ctx, sf.closeCancel = context.WithCancel(context.Background()) 99 | sf.rwMux.Unlock() 100 | defer sf.setConnectStatus(initial) 101 | 102 | for { 103 | select { 104 | case <-ctx.Done(): 105 | return 106 | default: 107 | } 108 | 109 | sf.Debug("connecting server %+v", sf.option.server) 110 | conn, err := openConnection(sf.option.server, sf.option.TLSConfig, sf.config.ConnectTimeout0) 111 | if err != nil { 112 | sf.Error("connect failed, %v", err) 113 | if !sf.option.autoReconnect { 114 | return 115 | } 116 | time.Sleep(sf.option.reconnectInterval) 117 | continue 118 | } 119 | sf.Debug("connect success") 120 | sf.conn = conn 121 | sf.onConnect(sf) 122 | sf.run(ctx) 123 | sf.onConnectionLost(sf) 124 | sf.Debug("disconnected server %+v", sf.option.server) 125 | select { 126 | case <-ctx.Done(): 127 | return 128 | default: 129 | // 随机500ms-1s的重试,避免快速重试造成服务器许多无效连接 130 | time.Sleep(time.Millisecond * time.Duration(500+rand.Intn(500))) 131 | } 132 | } 133 | } 134 | 135 | func (sf *serverSpec) IsClosed() bool { 136 | return sf.connectStatus() == initial 137 | } 138 | 139 | func (sf *serverSpec) Close() error { 140 | sf.rwMux.Lock() 141 | if sf.closeCancel != nil { 142 | sf.closeCancel() 143 | } 144 | sf.rwMux.Unlock() 145 | return nil 146 | } 147 | -------------------------------------------------------------------------------- /revive.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | revive -config revive.toml -formatter friendly ./... 4 | golangci-lint run -------------------------------------------------------------------------------- /revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "error" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.time-naming] 22 | [rule.unexported-return] 23 | [rule.indent-error-flow] 24 | [rule.errorf] 25 | [rule.empty-block] 26 | [rule.superfluous-else] 27 | [rule.unused-parameter] 28 | [rule.unreachable-code] 29 | [rule.redefines-builtin-id] 30 | [rule.receiver-naming] 31 | 32 | # Currently this makes too much noise, but should add it in 33 | # and perhaps ignore it in a few files 34 | #[rule.confusing-naming] 35 | # severity = "warning" 36 | #[rule.confusing-results] 37 | # severity = "warning" 38 | #[rule.unused-parameter] 39 | # severity = "warning" 40 | #[rule.deep-exit] 41 | # severity = "warning" 42 | #[rule.flag-parameter] 43 | # severity = "warning" 44 | 45 | --------------------------------------------------------------------------------