├── .gitignore ├── .golangci.yml ├── Makefile ├── README.md ├── canopen.go ├── dic_array.go ├── dic_eds_parser.go ├── dic_eds_parser_test.go ├── dic_object.go ├── dic_object_dic.go ├── dic_parser.go ├── dic_record.go ├── dic_types.go ├── dic_variable.go ├── go.mod ├── go.sum ├── network.go ├── network_frames_chan.go ├── network_test.go ├── nmt_master.go ├── node.go ├── pdo_map.go ├── pdo_maps.go ├── pdo_node.go ├── sdo.go ├── sdo_reader.go ├── sdo_writer.go └── utils ├── contains.go └── contains_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/node,vim,osx 3 | # Edit at https://www.gitignore.io/?templates=node,vim,osx 4 | 5 | ### Node ### 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | lerna-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | build/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # TypeScript v1 declaration files 50 | typings/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional REPL history 62 | .node_repl_history 63 | 64 | # Output of 'npm pack' 65 | *.tgz 66 | 67 | # Yarn Integrity file 68 | .yarn-integrity 69 | 70 | # dotenv environment variables file 71 | .env 72 | .env.test 73 | 74 | # parcel-bundler cache (https://parceljs.org/) 75 | .cache 76 | 77 | # next.js build output 78 | .next 79 | 80 | # nuxt.js build output 81 | .nuxt 82 | 83 | # vuepress build output 84 | .vuepress/dist 85 | 86 | # Serverless directories 87 | .serverless/ 88 | 89 | # FuseBox cache 90 | .fusebox/ 91 | 92 | # DynamoDB Local files 93 | .dynamodb/ 94 | 95 | ### OSX ### 96 | # General 97 | .DS_Store 98 | .AppleDouble 99 | .LSOverride 100 | 101 | # Icon must end with two \r 102 | Icon 103 | 104 | # Thumbnails 105 | ._* 106 | 107 | # Files that might appear in the root of a volume 108 | .DocumentRevisions-V100 109 | .fseventsd 110 | .Spotlight-V100 111 | .TemporaryItems 112 | .Trashes 113 | .VolumeIcon.icns 114 | .com.apple.timemachine.donotpresent 115 | 116 | # Directories potentially created on remote AFP share 117 | .AppleDB 118 | .AppleDesktop 119 | Network Trash Folder 120 | Temporary Items 121 | .apdisk 122 | 123 | ### Vim ### 124 | # Swap 125 | [._]*.s[a-v][a-z] 126 | [._]*.sw[a-p] 127 | [._]s[a-rt-v][a-z] 128 | [._]ss[a-gi-z] 129 | [._]sw[a-p] 130 | 131 | # Session 132 | Session.vim 133 | Sessionx.vim 134 | 135 | # Temporary 136 | .netrwhist 137 | *~ 138 | # Auto-generated tag files 139 | tags 140 | # Persistent undo 141 | [._]*.un~ 142 | 143 | # End of https://www.gitignore.io/api/node,vim,osx 144 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | golint: 3 | min-confidence: 0 4 | funlen: 5 | lines: 100 6 | statements: 80 7 | 8 | linters: 9 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 10 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 11 | disable-all: true 12 | enable: 13 | - bodyclose 14 | - deadcode 15 | - depguard 16 | - dogsled 17 | # - dupl 18 | - errcheck 19 | - funlen 20 | - gochecknoinits 21 | - goconst 22 | - gocritic 23 | - gocyclo 24 | - gofmt 25 | - goimports 26 | - golint 27 | - gosec 28 | - gosimple 29 | - govet 30 | - ineffassign 31 | - interfacer 32 | - lll 33 | - misspell 34 | - nakedret 35 | - scopelint 36 | - staticcheck 37 | - structcheck 38 | - stylecheck 39 | - typecheck 40 | - unconvert 41 | - unparam 42 | - unused 43 | - varcheck 44 | - whitespace 45 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOBIN=go 2 | 3 | # Lint code with golint 4 | .PHONY: lint 5 | lint: 6 | find . -name "*.go" | xargs misspell -error 7 | golint -set_exit_status ./... 8 | go vet ./... 9 | staticcheck ./... 10 | 11 | # Clean 12 | .PHONY: clean 13 | clean: 14 | rm -Rf ./coverage.out 15 | 16 | .PHONY: test 17 | test: 18 | go test -v -count 1 -race --coverprofile=coverage.out ./... 19 | go tool cover -func=coverage.out 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-canopen 2 | 3 | Implement canopen protocol with [https://github.com/angelodlfrtr/go-can](https://github.com/angelodlfrtr/go-can) 4 | 5 | Port of [https://github.com/christiansandberg/canopen](https://github.com/christiansandberg/canopen) 6 | written in Python 7 | 8 | ## Installation 9 | 10 | ```bash 11 | go get github.com/angelodlfrtr/go-canopen 12 | ``` 13 | 14 | ## Basic usage 15 | 16 | ```go 17 | package main 18 | 19 | import ( 20 | "github.com/angelodlfrtr/go-can" 21 | "github.com/angelodlfrtr/go-canopen" 22 | "github.com/angelodlfrtr/go-can/transports" 23 | "log" 24 | ) 25 | 26 | func main() { 27 | } 28 | ``` 29 | 30 | ## License 31 | 32 | Copyright (c) 2019 The go-canopen contributors 33 | 34 | Permission is hereby granted, free of charge, to any person obtaining a copy of 35 | this software and associated documentation files (the "Software"), 36 | to deal in the Software without restriction, including without limitation the 37 | rights to use, copy, modify, merge, publish, distribute, sublicense, and/or 38 | sell copies of the Software, and to permit persons to whom the Software is 39 | furnished to do so, subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in 42 | all copies or substantial portions of the Software. 43 | 44 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 45 | OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 46 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 47 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 48 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 49 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 50 | -------------------------------------------------------------------------------- /canopen.go: -------------------------------------------------------------------------------- 1 | // Package canopen implement canopen protocol over github.com/angelodlfrtr/go-can 2 | package canopen 3 | -------------------------------------------------------------------------------- /dic_array.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | type DicArray struct { 4 | Description string 5 | Index uint16 6 | Name string 7 | 8 | SDOClient *SDOClient 9 | 10 | SubIndexes map[uint8]DicObject 11 | SubNames map[string]uint8 12 | } 13 | 14 | func (array *DicArray) GetIndex() uint16 { 15 | return array.Index 16 | } 17 | 18 | func (array *DicArray) GetSubIndex() uint8 { return 0 } 19 | 20 | func (array *DicArray) GetName() string { 21 | return array.Name 22 | } 23 | 24 | // AddMember to SubIndexes 25 | func (array *DicArray) AddMember(object DicObject) { 26 | if array.SubIndexes == nil { 27 | array.SubIndexes = map[uint8]DicObject{} 28 | } 29 | 30 | if array.SubNames == nil { 31 | array.SubNames = map[string]uint8{} 32 | } 33 | 34 | array.SubIndexes[object.GetSubIndex()] = object 35 | array.SubNames[object.GetName()] = object.GetSubIndex() 36 | } 37 | 38 | func (array *DicArray) FindIndex(index uint16) DicObject { 39 | if object, ok := array.SubIndexes[uint8(index)]; ok { 40 | object.SetSDO(array.SDOClient) 41 | return object 42 | } 43 | 44 | return nil 45 | } 46 | 47 | func (array *DicArray) FindName(name string) DicObject { 48 | if index, ok := array.SubNames[name]; ok { 49 | return array.FindIndex(uint16(index)) 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (array *DicArray) GetDataType() byte { return 0x00 } 56 | func (array *DicArray) GetDataLen() int { return 0 } 57 | func (array *DicArray) SetSize(s int) {} 58 | func (array *DicArray) SetOffset(s int) {} 59 | func (array *DicArray) GetOffset() int { return 0 } 60 | func (array *DicArray) Read() error { return nil } 61 | func (array *DicArray) Save() error { return nil } 62 | func (array *DicArray) GetData() []byte { return nil } 63 | func (array *DicArray) SetData(data []byte) {} 64 | func (array *DicArray) GetStringVal() *string { return nil } 65 | func (array *DicArray) GetFloatVal() *float64 { return nil } 66 | func (array *DicArray) GetUintVal() *uint64 { return nil } 67 | func (array *DicArray) GetIntVal() *int64 { return nil } 68 | func (array *DicArray) GetBoolVal() *bool { return nil } 69 | func (array *DicArray) GetByteVal() *byte { return nil } 70 | func (array *DicArray) SetStringVal(a string) {} 71 | func (array *DicArray) SetFloatVal(a float64) {} 72 | func (array *DicArray) SetUintVal(a uint64) {} 73 | func (array *DicArray) SetIntVal(a int64) {} 74 | func (array *DicArray) SetBoolVal(a bool) {} 75 | func (array *DicArray) SetByteVal(a byte) {} 76 | func (array *DicArray) IsDicVariable() bool { return false } 77 | 78 | func (array *DicArray) SetSDO(sdo *SDOClient) { 79 | array.SDOClient = sdo 80 | } 81 | -------------------------------------------------------------------------------- /dic_eds_parser.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | 9 | "gopkg.in/ini.v1" 10 | ) 11 | 12 | // DicEDSParse If in is string, it must be a path to a file 13 | // else if in must be eds data as []byte 14 | func DicEDSParse(in interface{}) (*DicObjectDic, error) { 15 | // Load ini file 16 | iniData, err := ini.Load(in) 17 | if err != nil { 18 | return nil, nil 19 | } 20 | 21 | // Create object dictionary 22 | ddic := NewDicObjectDic() 23 | 24 | // Get NodeID & Baudrate 25 | if sec, err := iniData.GetSection("DeviceComissioning"); err == nil { 26 | // Get NodeID 27 | if key, err := sec.GetKey("NodeId"); err == nil { 28 | ab, _ := strconv.ParseInt(key.String(), 0, 0) 29 | ddic.NodeID = int(ab) 30 | } 31 | 32 | // Get Baudrate 33 | if key, err := sec.GetKey("Baudrate"); err == nil { 34 | ab, _ := strconv.ParseInt(key.String(), 0, 0) 35 | ddic.Baudrate = int(ab) 36 | } 37 | } 38 | 39 | matchIdxRegexp := regexp.MustCompile(`^[0-9A-Fa-f]{4}$`) 40 | matchSubIdxRegexp := regexp.MustCompile(`^([0-9A-Fa-f]{4})sub([0-9A-Fa-f]+)$`) 41 | 42 | // Iterate over sections 43 | for _, sec := range iniData.Sections() { 44 | sectionName := sec.Name() 45 | 46 | // Match index 47 | if matchIdxRegexp.MatchString(sectionName) { 48 | idx, err := strconv.ParseUint(sectionName, 16, 16) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | index := uint16(idx) 54 | 55 | name := sec.Key("ParameterName").String() 56 | objectType, _ := strconv.ParseUint(sec.Key("ObjectType").String(), 0, 8) 57 | 58 | // Object type == VARIABLE 59 | if byte(objectType) == DicVar || byte(objectType) == Domain { 60 | variable, err := buildVariable(index, 0, name, sec, iniData) 61 | if err != nil { 62 | return nil, err 63 | } 64 | ddic.AddObject(variable) 65 | } 66 | 67 | // Object type == ARRAY 68 | if byte(objectType) == DicArr { 69 | array := &DicArray{Index: index, Name: name} 70 | ddic.AddObject(array) 71 | } 72 | 73 | // Object type == RECORD 74 | if byte(objectType) == DicRec { 75 | record := &DicRecord{Index: index, Name: name} 76 | ddic.AddObject(record) 77 | } 78 | 79 | continue 80 | } 81 | 82 | // Match sub-indexs 83 | if matchSubIdxRegexp.MatchString(sectionName) { 84 | idx, err := strconv.ParseUint(sectionName[0:4], 16, 16) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | index := uint16(idx) 90 | sidx, err := strconv.ParseUint(sectionName[7:], 16, 8) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | subIndex := uint8(sidx) 96 | name := sec.Key("ParameterName").String() 97 | 98 | object := ddic.FindIndex(index) 99 | if object == nil { 100 | return nil, fmt.Errorf("index with id %d not found", index) 101 | } 102 | 103 | variable, err := buildVariable(index, subIndex, name, sec, iniData) 104 | if err != nil { 105 | return nil, err 106 | } 107 | object.AddMember(variable) 108 | } 109 | 110 | // @TODO: Match [index]Name 111 | } 112 | 113 | return ddic, nil 114 | } 115 | 116 | // @TODO: check working 117 | func buildVariable( 118 | index uint16, 119 | subIndex uint8, 120 | name string, 121 | sec *ini.Section, 122 | iniData *ini.File, 123 | ) (*DicVariable, error) { 124 | variable := &DicVariable{ 125 | Index: index, 126 | SubIndex: subIndex, 127 | Name: name, 128 | AccessType: strings.ToLower(sec.Key("AccessType").String()), 129 | } 130 | 131 | // Get & set DataType 132 | i, err := sec.Key("DataType").Int() 133 | if err != nil { 134 | return nil, err 135 | } 136 | variable.DataType = byte(i) 137 | 138 | if variable.DataType > 0x1B { 139 | dTypeStr := fmt.Sprintf("%dsub1", variable.DataType) 140 | 141 | i, err := iniData.Section(dTypeStr).Key("DefaultValue").Uint() 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | variable.DataType = byte(i) 147 | } 148 | 149 | if lowl, err := sec.GetKey("LowLimit"); err == nil { 150 | i, err := lowl.Int() 151 | if err != nil { 152 | return nil, err 153 | } 154 | variable.Min = i 155 | } 156 | 157 | if howl, err := sec.GetKey("HighLimit"); err == nil { 158 | i, err := howl.Int() 159 | if err != nil { 160 | return nil, err 161 | } 162 | variable.Max = i 163 | } 164 | 165 | if def, err := sec.GetKey("DefaultValue"); err == nil { 166 | variable.Default = []byte(def.Value()) 167 | } 168 | 169 | return variable, nil 170 | } 171 | -------------------------------------------------------------------------------- /dic_eds_parser_test.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | const TestEDSFile string = ` 8 | [Comments] 9 | 10 | Lines=2 11 | 12 | Line1=EDS file for AFE CANopen Slave 13 | 14 | Line2= 15 | 16 | 17 | 18 | [FileInfo] 19 | 20 | FileName=E:\PROJEKT\CANOPEN_EDS\SEAFE.eds 21 | 22 | FileVersion=1 23 | 24 | FileRevision=0 25 | 26 | EDSVersion=4.0 27 | 28 | Description=EDS of the AFE 29 | 30 | CreationTime=11:35AM 31 | 32 | CreationDate=01-20-2004 33 | 34 | CreatedBy=S.T.I.E. 35 | 36 | ModificationTime=10:57AM 37 | 38 | ModificationDate=04-02-2009 39 | 40 | ModifiedBy=S.T.I.E. 41 | 42 | 43 | 44 | [DeviceInfo] 45 | 46 | Vendorname=Schneider Electric 47 | 48 | VendorNumber=0x0200005A 49 | 50 | ProductName=AFE_V1.0 51 | 52 | ProductNumber=0x00414645 53 | 54 | RevisionNumber=0x00010000 55 | 56 | OrderCode=0 57 | 58 | BaudRate_10=0 59 | 60 | BaudRate_20=1 61 | 62 | BaudRate_50=1 63 | 64 | BaudRate_125=1 65 | 66 | BaudRate_250=1 67 | 68 | BaudRate_500=1 69 | 70 | BaudRate_800=0 71 | 72 | BaudRate_1000=1 73 | 74 | SimpleBootUpMaster=0 75 | 76 | SimpleBootUpSlave=1 77 | 78 | Granularity=0 79 | 80 | DynamicChannelsSupported=0 81 | 82 | GroupMessaging=0 83 | 84 | NrOfRXPDO=2 85 | 86 | NrOfTXPDO=2 87 | 88 | LSS_Supported=0 89 | 90 | CompactPDO=0x00 91 | 92 | 93 | 94 | [DummyUsage] 95 | 96 | Dummy0001=1 97 | 98 | Dummy0002=1 99 | 100 | Dummy0003=1 101 | 102 | Dummy0004=1 103 | 104 | Dummy0005=1 105 | 106 | Dummy0006=1 107 | 108 | Dummy0007=1 109 | 110 | 111 | 112 | [MandatoryObjects] 113 | 114 | SupportedObjects=3 115 | 116 | 1=0x1000 117 | 118 | 2=0x1001 119 | 120 | 3=0x1018 121 | 122 | 123 | 124 | [1000] 125 | 126 | ParameterName=Device Type 127 | 128 | ObjectType=0x7 129 | 130 | DataType=0x0007 131 | 132 | LowLimit= 133 | 134 | HighLimit= 135 | 136 | AccessType=ro 137 | 138 | DefaultValue=0x00000000 139 | 140 | PDOMapping=0 141 | 142 | ObjFlags=0x0 143 | 144 | 145 | 146 | [1001] 147 | 148 | ParameterName=Error Register 149 | 150 | ObjectType=0x7 151 | 152 | DataType=0x0005 153 | 154 | LowLimit= 155 | 156 | HighLimit= 157 | 158 | AccessType=ro 159 | 160 | DefaultValue=0x0 161 | 162 | PDOMapping=0 163 | 164 | ObjFlags=0x0 165 | 166 | 167 | 168 | [1018] 169 | 170 | ParameterName=Identity Object 171 | 172 | SubNumber=4 173 | 174 | ObjectType=0x8 175 | 176 | 177 | 178 | [1018sub0] 179 | 180 | ParameterName=Number of entries 181 | 182 | ObjectType=0x7 183 | 184 | DataType=0x0005 185 | 186 | LowLimit=3 187 | 188 | HighLimit=3 189 | 190 | AccessType=ro 191 | 192 | DefaultValue=3 193 | 194 | PDOMapping=0 195 | 196 | ObjFlags=0x0 197 | 198 | 199 | 200 | [1018sub1] 201 | 202 | ParameterName=Vendor ID 203 | 204 | ObjectType=0x7 205 | 206 | DataType=0x0007 207 | 208 | LowLimit= 209 | 210 | HighLimit= 211 | 212 | AccessType=ro 213 | 214 | DefaultValue=0x0200005A 215 | 216 | PDOMapping=0 217 | 218 | ObjFlags=0x0 219 | 220 | 221 | 222 | [1018sub2] 223 | 224 | ParameterName=Product code 225 | 226 | ObjectType=0x7 227 | 228 | DataType=0x0007 229 | 230 | LowLimit= 231 | 232 | HighLimit= 233 | 234 | AccessType=ro 235 | 236 | DefaultValue=0x00414645 237 | 238 | PDOMapping=0 239 | 240 | ObjFlags=0x0 241 | 242 | 243 | 244 | [1018sub3] 245 | 246 | ParameterName=Revision number 247 | 248 | ObjectType=0x7 249 | 250 | DataType=0x0007 251 | 252 | LowLimit= 253 | 254 | HighLimit= 255 | 256 | AccessType=ro 257 | 258 | DefaultValue=0x00010000 259 | 260 | PDOMapping=0 261 | 262 | ObjFlags=0x0 263 | 264 | 265 | 266 | [OptionalObjects] 267 | 268 | SupportedObjects=19 269 | 270 | 1=0x1003 271 | 272 | 2=0x1005 273 | 274 | 3=0x1008 275 | 276 | 4=0x100B 277 | 278 | 5=0x100C 279 | 280 | 6=0x100D 281 | 282 | 7=0x100E 283 | 284 | 8=0x100F 285 | 286 | 9=0x1014 287 | 288 | 10=0x1016 289 | 290 | 11=0x1017 291 | 292 | 12=0x1400 293 | 294 | 13=0x1401 295 | 296 | 14=0x1600 297 | 298 | 15=0x1601 299 | 300 | 16=0x1800 301 | 302 | 17=0x1801 303 | 304 | 18=0x1A00 305 | 306 | 19=0x1A01 307 | 308 | 309 | 310 | [1003] 311 | 312 | ParameterName=Pre-defined Error Field 313 | 314 | SubNumber=2 315 | 316 | ObjectType=0x8 317 | 318 | 319 | 320 | [1003sub0] 321 | 322 | ParameterName=Number of error 323 | 324 | ObjectType=0x7 325 | 326 | DataType=0x0005 327 | 328 | LowLimit= 329 | 330 | HighLimit= 331 | 332 | AccessType=ro 333 | 334 | DefaultValue=0 335 | 336 | PDOMapping=0 337 | 338 | ObjFlags=0x0 339 | 340 | 341 | 342 | [1003sub1] 343 | 344 | ParameterName=Standard Error Field 345 | 346 | ObjectType=0x7 347 | 348 | DataType=0x0007 349 | 350 | LowLimit= 351 | 352 | HighLimit= 353 | 354 | AccessType=ro 355 | 356 | DefaultValue=0 357 | 358 | PDOMapping=0 359 | 360 | ObjFlags=0x0 361 | 362 | 363 | 364 | [1005] 365 | 366 | ParameterName=COB-ID SYNC message 367 | 368 | ObjectType=0x7 369 | 370 | DataType=0x0007 371 | 372 | LowLimit= 373 | 374 | HighLimit= 375 | 376 | AccessType=rw 377 | 378 | DefaultValue=0x80 379 | 380 | PDOMapping=0 381 | 382 | ObjFlags=0x0 383 | 384 | 385 | 386 | [1008] 387 | 388 | ParameterName=Device Name 389 | 390 | ObjectType=0x7 391 | 392 | DataType=0x0009 393 | 394 | LowLimit= 395 | 396 | HighLimit= 397 | 398 | AccessType=const 399 | 400 | DefaultValue=AFE 401 | 402 | PDOMapping=0 403 | 404 | ObjFlags=0x0 405 | 406 | 407 | 408 | [100B] 409 | 410 | ParameterName=NodeID 411 | 412 | ObjectType=0x7 413 | 414 | DataType=0x0005 415 | 416 | LowLimit= 417 | 418 | HighLimit= 419 | 420 | AccessType=ro 421 | 422 | DefaultValue= 423 | 424 | PDOMapping=0 425 | 426 | ObjFlags=0x0 427 | 428 | 429 | 430 | [100C] 431 | 432 | ParameterName=Guard Time 433 | 434 | ObjectType=0x7 435 | 436 | DataType=0x0006 437 | 438 | LowLimit= 439 | 440 | HighLimit= 441 | 442 | AccessType=rw 443 | 444 | DefaultValue=0 445 | 446 | PDOMapping=0 447 | 448 | ObjFlags=0x0 449 | 450 | 451 | 452 | [100D] 453 | 454 | ParameterName=Life Time Factor 455 | 456 | ObjectType=0x7 457 | 458 | DataType=0x0005 459 | 460 | LowLimit= 461 | 462 | HighLimit= 463 | 464 | AccessType=rw 465 | 466 | DefaultValue=0 467 | 468 | PDOMapping=0 469 | 470 | ObjFlags=0x0 471 | 472 | 473 | 474 | [100E] 475 | 476 | ParameterName=Node Guarding Identifier 477 | 478 | ObjectType=0x7 479 | 480 | DataType=0x0007 481 | 482 | LowLimit= 483 | 484 | HighLimit= 485 | 486 | AccessType=ro 487 | 488 | DefaultValue=$NodeID + 0x700 489 | 490 | PDOMapping=0 491 | 492 | ObjFlags=0x0 493 | 494 | 495 | 496 | [100F] 497 | 498 | ParameterName=Number of SDO supported 499 | 500 | ObjectType=0x7 501 | 502 | DataType=0x0007 503 | 504 | LowLimit= 505 | 506 | HighLimit= 507 | 508 | AccessType=ro 509 | 510 | DefaultValue=1 511 | 512 | PDOMapping=0 513 | 514 | ObjFlags=0x0 515 | 516 | 517 | 518 | [1014] 519 | 520 | ParameterName=COB-ID Emergency message 521 | 522 | ObjectType=0x7 523 | 524 | DataType=0x0007 525 | 526 | LowLimit= 527 | 528 | HighLimit= 529 | 530 | AccessType=ro 531 | 532 | DefaultValue=$NodeID + 0x080 533 | 534 | PDOMapping=0 535 | 536 | ObjFlags=0x0 537 | 538 | 539 | 540 | [1016] 541 | 542 | ParameterName=Consumer Heartbeat Time 543 | 544 | SubNumber=2 545 | 546 | ObjectType=0x8 547 | 548 | 549 | 550 | [1016sub0] 551 | 552 | ParameterName=Number of entries 553 | 554 | ObjectType=0x7 555 | 556 | DataType=0x0005 557 | 558 | LowLimit= 559 | 560 | HighLimit= 561 | 562 | AccessType=ro 563 | 564 | DefaultValue=1 565 | 566 | PDOMapping=0 567 | 568 | ObjFlags=0x0 569 | 570 | 571 | 572 | [1016sub1] 573 | 574 | ParameterName=Consumer Heartbeat Time 575 | 576 | ObjectType=0x7 577 | 578 | DataType=0x0007 579 | 580 | LowLimit= 581 | 582 | HighLimit= 583 | 584 | AccessType=rw 585 | 586 | DefaultValue=0 587 | 588 | PDOMapping=0 589 | 590 | ObjFlags=0x0 591 | 592 | 593 | 594 | [1017] 595 | 596 | ParameterName=Producer Heartbeat Time 597 | 598 | ObjectType=0x7 599 | 600 | DataType=0x0006 601 | 602 | LowLimit= 603 | 604 | HighLimit= 605 | 606 | AccessType=rw 607 | 608 | DefaultValue=0 609 | 610 | PDOMapping=0 611 | 612 | ObjFlags=0x0 613 | 614 | 615 | 616 | [1400] 617 | 618 | ParameterName=Receive PDO1 parameter 619 | 620 | SubNumber=3 621 | 622 | ObjectType=0x9 623 | 624 | 625 | 626 | [1400sub0] 627 | 628 | ParameterName=Number of entries 629 | 630 | ObjectType=0x7 631 | 632 | DataType=0x0005 633 | 634 | LowLimit= 635 | 636 | HighLimit= 637 | 638 | AccessType=ro 639 | 640 | DefaultValue=2 641 | 642 | PDOMapping=0 643 | 644 | ObjFlags=0x0 645 | 646 | 647 | 648 | [1400sub1] 649 | 650 | ParameterName=COB-ID 651 | 652 | ObjectType=0x7 653 | 654 | DataType=0x0007 655 | 656 | LowLimit= 657 | 658 | HighLimit= 659 | 660 | AccessType=rw 661 | 662 | DefaultValue=$NodeID + 0x0200 663 | 664 | PDOMapping=0 665 | 666 | ObjFlags=0x0 667 | 668 | 669 | 670 | [1400sub2] 671 | 672 | ParameterName=Transmission type 673 | 674 | ObjectType=0x7 675 | 676 | DataType=0x0005 677 | 678 | LowLimit= 679 | 680 | HighLimit= 681 | 682 | AccessType=rw 683 | 684 | DefaultValue=255 685 | 686 | PDOMapping=0 687 | 688 | ObjFlags=0x0 689 | 690 | 691 | 692 | [1401] 693 | 694 | ParameterName=Receive PDO2 parameter 695 | 696 | SubNumber=3 697 | 698 | ObjectType=0x9 699 | 700 | 701 | 702 | [1401sub0] 703 | 704 | ParameterName=Number of entries 705 | 706 | ObjectType=0x7 707 | 708 | DataType=0x0005 709 | 710 | LowLimit= 711 | 712 | HighLimit= 713 | 714 | AccessType=ro 715 | 716 | DefaultValue=2 717 | 718 | PDOMapping=0 719 | 720 | ObjFlags=0x0 721 | 722 | 723 | 724 | [1401sub1] 725 | 726 | ParameterName=COB-ID 727 | 728 | ObjectType=0x7 729 | 730 | DataType=0x0007 731 | 732 | LowLimit= 733 | 734 | HighLimit= 735 | 736 | AccessType=rw 737 | 738 | DefaultValue=$NodeID + 0x80000300 739 | 740 | PDOMapping=0 741 | 742 | ObjFlags=0x0 743 | 744 | 745 | 746 | [1401sub2] 747 | 748 | ParameterName=Transmission type 749 | 750 | ObjectType=0x7 751 | 752 | DataType=0x0005 753 | 754 | LowLimit= 755 | 756 | HighLimit= 757 | 758 | AccessType=rw 759 | 760 | DefaultValue=255 761 | 762 | PDOMapping=0 763 | 764 | ObjFlags=0x0 765 | 766 | 767 | 768 | [1600] 769 | 770 | ParameterName=Receive PDO1 mapping 771 | 772 | SubNumber=5 773 | 774 | ObjectType=0x8 775 | 776 | 777 | 778 | [1600sub0] 779 | 780 | ParameterName=Number of mapped objects 781 | 782 | ObjectType=0x7 783 | 784 | DataType=0x0005 785 | 786 | LowLimit=0 787 | 788 | HighLimit=4 789 | 790 | AccessType=ro 791 | 792 | DefaultValue=4 793 | 794 | PDOMapping=0 795 | 796 | ObjFlags=0x0 797 | 798 | 799 | 800 | [1600sub1] 801 | 802 | ParameterName=1.mapped object 803 | 804 | ObjectType=0x7 805 | 806 | DataType=0x0007 807 | 808 | LowLimit= 809 | 810 | HighLimit= 811 | 812 | AccessType=ro 813 | 814 | DefaultValue=0x30000110 815 | 816 | PDOMapping=0 817 | 818 | ObjFlags=0x3 819 | 820 | 821 | 822 | [1600sub2] 823 | 824 | ParameterName=2.mapped object 825 | 826 | ObjectType=0x7 827 | 828 | DataType=0x0007 829 | 830 | LowLimit= 831 | 832 | HighLimit= 833 | 834 | AccessType=ro 835 | 836 | DefaultValue=0x30000210 837 | 838 | PDOMapping=0 839 | 840 | ObjFlags=0x3 841 | 842 | 843 | 844 | [1600sub3] 845 | 846 | ParameterName=3.mapped object 847 | 848 | ObjectType=0x7 849 | 850 | DataType=0x0007 851 | 852 | LowLimit= 853 | 854 | HighLimit= 855 | 856 | AccessType=ro 857 | 858 | DefaultValue=0x30000310 859 | 860 | PDOMapping=0 861 | 862 | ObjFlags=0x3 863 | 864 | 865 | 866 | [1600sub4] 867 | 868 | ParameterName=4.mapped object 869 | 870 | ObjectType=0x7 871 | 872 | DataType=0x0007 873 | 874 | LowLimit= 875 | 876 | HighLimit= 877 | 878 | AccessType=ro 879 | 880 | DefaultValue=0x30000410 881 | 882 | PDOMapping=0 883 | 884 | ObjFlags=0x3 885 | 886 | 887 | 888 | [1601] 889 | 890 | ParameterName=Receive PDO2 mapping 891 | 892 | SubNumber=5 893 | 894 | ObjectType=0x8 895 | 896 | 897 | 898 | [1601sub0] 899 | 900 | ParameterName=Number of mapped objects 901 | 902 | ObjectType=0x7 903 | 904 | DataType=0x0005 905 | 906 | LowLimit=0 907 | 908 | HighLimit=4 909 | 910 | AccessType=ro 911 | 912 | DefaultValue=4 913 | 914 | PDOMapping=0 915 | 916 | ObjFlags=0x0 917 | 918 | 919 | 920 | [1601sub1] 921 | 922 | ParameterName=1.mapped object 923 | 924 | ObjectType=0x7 925 | 926 | DataType=0x0007 927 | 928 | LowLimit= 929 | 930 | HighLimit= 931 | 932 | AccessType=ro 933 | 934 | DefaultValue=0x30000510 935 | 936 | PDOMapping=0 937 | 938 | ObjFlags=0x3 939 | 940 | 941 | 942 | [1601sub2] 943 | 944 | ParameterName=2.mapped object 945 | 946 | ObjectType=0x7 947 | 948 | DataType=0x0007 949 | 950 | LowLimit= 951 | 952 | HighLimit= 953 | 954 | AccessType=ro 955 | 956 | DefaultValue=0x30000610 957 | 958 | PDOMapping=0 959 | 960 | ObjFlags=0x3 961 | 962 | 963 | 964 | [1601sub3] 965 | 966 | ParameterName=3.mapped object 967 | 968 | ObjectType=0x7 969 | 970 | DataType=0x0007 971 | 972 | LowLimit= 973 | 974 | HighLimit= 975 | 976 | AccessType=ro 977 | 978 | DefaultValue=0x30000710 979 | 980 | PDOMapping=0 981 | 982 | ObjFlags=0x3 983 | 984 | 985 | 986 | [1601sub4] 987 | 988 | ParameterName=4.mapped object 989 | 990 | ObjectType=0x7 991 | 992 | DataType=0x0007 993 | 994 | LowLimit= 995 | 996 | HighLimit= 997 | 998 | AccessType=ro 999 | 1000 | DefaultValue=0x30000810 1001 | 1002 | PDOMapping=0 1003 | 1004 | ObjFlags=0x3 1005 | 1006 | 1007 | 1008 | [1800] 1009 | 1010 | ParameterName=Transmit PDO1 parameter 1011 | 1012 | SubNumber=6 1013 | 1014 | ObjectType=0x9 1015 | 1016 | 1017 | 1018 | [1800sub0] 1019 | 1020 | ParameterName=Number of entries 1021 | 1022 | ObjectType=0x7 1023 | 1024 | DataType=0x0005 1025 | 1026 | LowLimit= 1027 | 1028 | HighLimit= 1029 | 1030 | AccessType=ro 1031 | 1032 | DefaultValue=5 1033 | 1034 | PDOMapping=0 1035 | 1036 | ObjFlags=0x0 1037 | 1038 | 1039 | 1040 | [1800sub1] 1041 | 1042 | ParameterName=COB-ID 1043 | 1044 | ObjectType=0x7 1045 | 1046 | DataType=0x0007 1047 | 1048 | LowLimit= 1049 | 1050 | HighLimit= 1051 | 1052 | AccessType=rw 1053 | 1054 | DefaultValue=$NodeID + 0x0180 1055 | 1056 | PDOMapping=0 1057 | 1058 | ObjFlags=0x0 1059 | 1060 | 1061 | 1062 | [1800sub2] 1063 | 1064 | ParameterName=Transmission type 1065 | 1066 | ObjectType=0x7 1067 | 1068 | DataType=0x0005 1069 | 1070 | LowLimit= 1071 | 1072 | HighLimit= 1073 | 1074 | AccessType=rw 1075 | 1076 | DefaultValue=255 1077 | 1078 | PDOMapping=0 1079 | 1080 | ObjFlags=0x0 1081 | 1082 | 1083 | 1084 | [1800sub3] 1085 | 1086 | ParameterName=Inhibit timer 1087 | 1088 | ObjectType=0x7 1089 | 1090 | DataType=0x0006 1091 | 1092 | LowLimit= 1093 | 1094 | HighLimit= 1095 | 1096 | AccessType=rw 1097 | 1098 | DefaultValue=300 1099 | 1100 | PDOMapping=0 1101 | 1102 | ObjFlags=0x0 1103 | 1104 | 1105 | 1106 | [1800sub4] 1107 | 1108 | ParameterName=reserved 1109 | 1110 | ObjectType=0x7 1111 | 1112 | DataType=0x0005 1113 | 1114 | LowLimit= 1115 | 1116 | HighLimit= 1117 | 1118 | AccessType=ro 1119 | 1120 | DefaultValue= 1121 | 1122 | PDOMapping=0 1123 | 1124 | ObjFlags=0x0 1125 | 1126 | 1127 | 1128 | [1800sub5] 1129 | 1130 | ParameterName=Event Timer 1131 | 1132 | ObjectType=0x7 1133 | 1134 | DataType=0x0006 1135 | 1136 | LowLimit= 1137 | 1138 | HighLimit= 1139 | 1140 | AccessType=rw 1141 | 1142 | DefaultValue=1000 1143 | 1144 | PDOMapping=0 1145 | 1146 | ObjFlags=0x0 1147 | 1148 | 1149 | 1150 | [1801] 1151 | 1152 | ParameterName=Transmit PDO2 parameter 1153 | 1154 | SubNumber=6 1155 | 1156 | ObjectType=0x9 1157 | 1158 | 1159 | 1160 | [1801sub0] 1161 | 1162 | ParameterName=Number of entries 1163 | 1164 | ObjectType=0x7 1165 | 1166 | DataType=0x0005 1167 | 1168 | LowLimit= 1169 | 1170 | HighLimit= 1171 | 1172 | AccessType=ro 1173 | 1174 | DefaultValue=5 1175 | 1176 | PDOMapping=0 1177 | 1178 | ObjFlags=0x0 1179 | 1180 | 1181 | 1182 | [1801sub1] 1183 | 1184 | ParameterName=COB-ID 1185 | 1186 | ObjectType=0x7 1187 | 1188 | DataType=0x0007 1189 | 1190 | LowLimit= 1191 | 1192 | HighLimit= 1193 | 1194 | AccessType=rw 1195 | 1196 | DefaultValue=$NodeID + 0x80000280 1197 | 1198 | PDOMapping=0 1199 | 1200 | ObjFlags=0x0 1201 | 1202 | 1203 | 1204 | [1801sub2] 1205 | 1206 | ParameterName=Transmission type 1207 | 1208 | ObjectType=0x7 1209 | 1210 | DataType=0x0005 1211 | 1212 | LowLimit= 1213 | 1214 | HighLimit= 1215 | 1216 | AccessType=rw 1217 | 1218 | DefaultValue=255 1219 | 1220 | PDOMapping=0 1221 | 1222 | ObjFlags=0x0 1223 | 1224 | 1225 | 1226 | [1801sub3] 1227 | 1228 | ParameterName=Inhibit timer 1229 | 1230 | ObjectType=0x7 1231 | 1232 | DataType=0x0006 1233 | 1234 | LowLimit= 1235 | 1236 | HighLimit= 1237 | 1238 | AccessType=rw 1239 | 1240 | DefaultValue=300 1241 | 1242 | PDOMapping=0 1243 | 1244 | ObjFlags=0x0 1245 | 1246 | 1247 | 1248 | [1801sub4] 1249 | 1250 | ParameterName=reserved 1251 | 1252 | ObjectType=0x7 1253 | 1254 | DataType=0x0005 1255 | 1256 | LowLimit= 1257 | 1258 | HighLimit= 1259 | 1260 | AccessType=ro 1261 | 1262 | DefaultValue= 1263 | 1264 | PDOMapping=0 1265 | 1266 | ObjFlags=0x0 1267 | 1268 | 1269 | 1270 | [1801sub5] 1271 | 1272 | ParameterName=Event Timer 1273 | 1274 | ObjectType=0x7 1275 | 1276 | DataType=0x0006 1277 | 1278 | LowLimit= 1279 | 1280 | HighLimit= 1281 | 1282 | AccessType=rw 1283 | 1284 | DefaultValue=1000 1285 | 1286 | PDOMapping=0 1287 | 1288 | ObjFlags=0x0 1289 | 1290 | 1291 | 1292 | [1A00] 1293 | 1294 | ParameterName=Transmit PDO1 mapping 1295 | 1296 | SubNumber=5 1297 | 1298 | ObjectType=0x8 1299 | 1300 | 1301 | 1302 | [1A00sub0] 1303 | 1304 | ParameterName=Number of mapped objects 1305 | 1306 | ObjectType=0x7 1307 | 1308 | DataType=0x0005 1309 | 1310 | LowLimit=0 1311 | 1312 | HighLimit=4 1313 | 1314 | AccessType=ro 1315 | 1316 | DefaultValue=4 1317 | 1318 | PDOMapping=0 1319 | 1320 | ObjFlags=0x0 1321 | 1322 | 1323 | 1324 | [1A00sub1] 1325 | 1326 | ParameterName=1.mapped object 1327 | 1328 | ObjectType=0x7 1329 | 1330 | DataType=0x0007 1331 | 1332 | LowLimit= 1333 | 1334 | HighLimit= 1335 | 1336 | AccessType=ro 1337 | 1338 | DefaultValue=0x30100110 1339 | 1340 | PDOMapping=0 1341 | 1342 | ObjFlags=0x3 1343 | 1344 | 1345 | 1346 | [1A00sub2] 1347 | 1348 | ParameterName=2.mapped object 1349 | 1350 | ObjectType=0x7 1351 | 1352 | DataType=0x0007 1353 | 1354 | LowLimit= 1355 | 1356 | HighLimit= 1357 | 1358 | AccessType=ro 1359 | 1360 | DefaultValue=0x30100210 1361 | 1362 | PDOMapping=0 1363 | 1364 | ObjFlags=0x3 1365 | 1366 | 1367 | 1368 | [1A00sub3] 1369 | 1370 | ParameterName=3.mapped object 1371 | 1372 | ObjectType=0x7 1373 | 1374 | DataType=0x0007 1375 | 1376 | LowLimit= 1377 | 1378 | HighLimit= 1379 | 1380 | AccessType=ro 1381 | 1382 | DefaultValue=0x30100310 1383 | 1384 | PDOMapping=0 1385 | 1386 | ObjFlags=0x3 1387 | 1388 | 1389 | 1390 | [1A00sub4] 1391 | 1392 | ParameterName=4.mapped object 1393 | 1394 | ObjectType=0x7 1395 | 1396 | DataType=0x0007 1397 | 1398 | LowLimit= 1399 | 1400 | HighLimit= 1401 | 1402 | AccessType=ro 1403 | 1404 | DefaultValue=0x30100410 1405 | 1406 | PDOMapping=0 1407 | 1408 | ObjFlags=0x3 1409 | 1410 | 1411 | 1412 | [1A01] 1413 | 1414 | ParameterName=Transmit PDO2 mapping 1415 | 1416 | SubNumber=5 1417 | 1418 | ObjectType=0x8 1419 | 1420 | 1421 | 1422 | [1A01sub0] 1423 | 1424 | ParameterName=Number of mapped objects 1425 | 1426 | ObjectType=0x7 1427 | 1428 | DataType=0x0005 1429 | 1430 | LowLimit=0 1431 | 1432 | HighLimit=4 1433 | 1434 | AccessType=ro 1435 | 1436 | DefaultValue=4 1437 | 1438 | PDOMapping=0 1439 | 1440 | ObjFlags=0x0 1441 | 1442 | 1443 | 1444 | [1A01sub1] 1445 | 1446 | ParameterName=1.mapped object 1447 | 1448 | ObjectType=0x7 1449 | 1450 | DataType=0x0007 1451 | 1452 | LowLimit= 1453 | 1454 | HighLimit= 1455 | 1456 | AccessType=ro 1457 | 1458 | DefaultValue=0x30100510 1459 | 1460 | PDOMapping=0 1461 | 1462 | ObjFlags=0x3 1463 | 1464 | 1465 | 1466 | [1A01sub2] 1467 | 1468 | ParameterName=2.mapped object 1469 | 1470 | ObjectType=0x7 1471 | 1472 | DataType=0x0007 1473 | 1474 | LowLimit= 1475 | 1476 | HighLimit= 1477 | 1478 | AccessType=ro 1479 | 1480 | DefaultValue=0x30100610 1481 | 1482 | PDOMapping=0 1483 | 1484 | ObjFlags=0x3 1485 | 1486 | 1487 | 1488 | [1A01sub3] 1489 | 1490 | ParameterName=3.mapped object 1491 | 1492 | ObjectType=0x7 1493 | 1494 | DataType=0x0007 1495 | 1496 | LowLimit= 1497 | 1498 | HighLimit= 1499 | 1500 | AccessType=ro 1501 | 1502 | DefaultValue=0x30100710 1503 | 1504 | PDOMapping=0 1505 | 1506 | ObjFlags=0x3 1507 | 1508 | 1509 | 1510 | [1A01sub4] 1511 | 1512 | ParameterName=4.mapped object 1513 | 1514 | ObjectType=0x7 1515 | 1516 | DataType=0x0007 1517 | 1518 | LowLimit= 1519 | 1520 | HighLimit= 1521 | 1522 | AccessType=ro 1523 | 1524 | DefaultValue=0x30100810 1525 | 1526 | PDOMapping=0 1527 | 1528 | ObjFlags=0x3 1529 | 1530 | 1531 | 1532 | [ManufacturerObjects] 1533 | 1534 | SupportedObjects=2 1535 | 1536 | 1=0x3000 1537 | 1538 | 2=0x3010 1539 | 1540 | 1541 | 1542 | [3000] 1543 | 1544 | ParameterName=controlword and target values 1545 | 1546 | SubNumber=9 1547 | 1548 | ObjectType=0x8 1549 | 1550 | 1551 | 1552 | [3000sub0] 1553 | 1554 | ParameterName=number of target values 1555 | 1556 | ObjectType=0x7 1557 | 1558 | DataType=0x0006 1559 | 1560 | LowLimit= 1561 | 1562 | HighLimit= 1563 | 1564 | AccessType=ro 1565 | 1566 | DefaultValue=8 1567 | 1568 | PDOMapping=0 1569 | 1570 | ObjFlags=0x0 1571 | 1572 | 1573 | 1574 | [3000sub1] 1575 | 1576 | ParameterName=controlword 1577 | 1578 | ObjectType=0x7 1579 | 1580 | DataType=0x0006 1581 | 1582 | LowLimit= 1583 | 1584 | HighLimit= 1585 | 1586 | AccessType=rww 1587 | 1588 | DefaultValue= 1589 | 1590 | PDOMapping=1 1591 | 1592 | ObjFlags=0x0 1593 | 1594 | 1595 | 1596 | [3000sub2] 1597 | 1598 | ParameterName=ref_value 1 1599 | 1600 | ObjectType=0x7 1601 | 1602 | DataType=0x0006 1603 | 1604 | LowLimit= 1605 | 1606 | HighLimit= 1607 | 1608 | AccessType=rww 1609 | 1610 | DefaultValue= 1611 | 1612 | PDOMapping=1 1613 | 1614 | ObjFlags=0x0 1615 | 1616 | 1617 | 1618 | [3000sub3] 1619 | 1620 | ParameterName=ref_value 2 1621 | 1622 | ObjectType=0x7 1623 | 1624 | DataType=0x0006 1625 | 1626 | LowLimit= 1627 | 1628 | HighLimit= 1629 | 1630 | AccessType=rww 1631 | 1632 | DefaultValue= 1633 | 1634 | PDOMapping=1 1635 | 1636 | ObjFlags=0x0 1637 | 1638 | 1639 | 1640 | [3000sub4] 1641 | 1642 | ParameterName=ref_value 3 1643 | 1644 | ObjectType=0x7 1645 | 1646 | DataType=0x0006 1647 | 1648 | LowLimit= 1649 | 1650 | HighLimit= 1651 | 1652 | AccessType=rww 1653 | 1654 | DefaultValue= 1655 | 1656 | PDOMapping=1 1657 | 1658 | ObjFlags=0x0 1659 | 1660 | 1661 | 1662 | [3000sub5] 1663 | 1664 | ParameterName=ref_value 4 1665 | 1666 | ObjectType=0x7 1667 | 1668 | DataType=0x0006 1669 | 1670 | LowLimit= 1671 | 1672 | HighLimit= 1673 | 1674 | AccessType=rww 1675 | 1676 | DefaultValue= 1677 | 1678 | PDOMapping=1 1679 | 1680 | ObjFlags=0x0 1681 | 1682 | 1683 | 1684 | [3000sub6] 1685 | 1686 | ParameterName=ref_value 5 1687 | 1688 | ObjectType=0x7 1689 | 1690 | DataType=0x0006 1691 | 1692 | LowLimit= 1693 | 1694 | HighLimit= 1695 | 1696 | AccessType=rww 1697 | 1698 | DefaultValue= 1699 | 1700 | PDOMapping=1 1701 | 1702 | ObjFlags=0x0 1703 | 1704 | 1705 | 1706 | [3000sub7] 1707 | 1708 | ParameterName=ref_value 6 1709 | 1710 | ObjectType=0x7 1711 | 1712 | DataType=0x0006 1713 | 1714 | LowLimit= 1715 | 1716 | HighLimit= 1717 | 1718 | AccessType=rww 1719 | 1720 | DefaultValue= 1721 | 1722 | PDOMapping=1 1723 | 1724 | ObjFlags=0x0 1725 | 1726 | 1727 | 1728 | [3000sub8] 1729 | 1730 | ParameterName=ref_value 7 1731 | 1732 | ObjectType=0x7 1733 | 1734 | DataType=0x0006 1735 | 1736 | LowLimit= 1737 | 1738 | HighLimit= 1739 | 1740 | AccessType=rww 1741 | 1742 | DefaultValue= 1743 | 1744 | PDOMapping=1 1745 | 1746 | ObjFlags=0x0 1747 | 1748 | 1749 | 1750 | [3010] 1751 | 1752 | ParameterName=statusword and actual values 1753 | 1754 | SubNumber=9 1755 | 1756 | ObjectType=0x8 1757 | 1758 | 1759 | 1760 | [3010sub0] 1761 | 1762 | ParameterName=number of actual values 1763 | 1764 | ObjectType=0x7 1765 | 1766 | DataType=0x0006 1767 | 1768 | LowLimit= 1769 | 1770 | HighLimit= 1771 | 1772 | AccessType=ro 1773 | 1774 | DefaultValue=8 1775 | 1776 | PDOMapping=0 1777 | 1778 | ObjFlags=0x0 1779 | 1780 | 1781 | 1782 | [3010sub1] 1783 | 1784 | ParameterName=statusword 1785 | 1786 | ObjectType=0x7 1787 | 1788 | DataType=0x0006 1789 | 1790 | LowLimit= 1791 | 1792 | HighLimit= 1793 | 1794 | AccessType=ro 1795 | 1796 | DefaultValue= 1797 | 1798 | PDOMapping=1 1799 | 1800 | ObjFlags=0x0 1801 | 1802 | 1803 | 1804 | [3010sub2] 1805 | 1806 | ParameterName=act_value 1 1807 | 1808 | ObjectType=0x7 1809 | 1810 | DataType=0x0006 1811 | 1812 | LowLimit= 1813 | 1814 | HighLimit= 1815 | 1816 | AccessType=ro 1817 | 1818 | DefaultValue= 1819 | 1820 | PDOMapping=1 1821 | 1822 | ObjFlags=0x0 1823 | 1824 | 1825 | 1826 | [3010sub3] 1827 | 1828 | ParameterName=act_value 2 1829 | 1830 | ObjectType=0x7 1831 | 1832 | DataType=0x0006 1833 | 1834 | LowLimit= 1835 | 1836 | HighLimit= 1837 | 1838 | AccessType=ro 1839 | 1840 | DefaultValue= 1841 | 1842 | PDOMapping=1 1843 | 1844 | ObjFlags=0x0 1845 | 1846 | 1847 | 1848 | [3010sub4] 1849 | 1850 | ParameterName=act_value 3 1851 | 1852 | ObjectType=0x7 1853 | 1854 | DataType=0x0006 1855 | 1856 | LowLimit= 1857 | 1858 | HighLimit= 1859 | 1860 | AccessType=ro 1861 | 1862 | DefaultValue= 1863 | 1864 | PDOMapping=1 1865 | 1866 | ObjFlags=0x0 1867 | 1868 | 1869 | 1870 | [3010sub5] 1871 | 1872 | ParameterName=act_value 4 1873 | 1874 | ObjectType=0x7 1875 | 1876 | DataType=0x0006 1877 | 1878 | LowLimit= 1879 | 1880 | HighLimit= 1881 | 1882 | AccessType=ro 1883 | 1884 | DefaultValue= 1885 | 1886 | PDOMapping=1 1887 | 1888 | ObjFlags=0x0 1889 | 1890 | 1891 | 1892 | [3010sub6] 1893 | 1894 | ParameterName=act_value 5 1895 | 1896 | ObjectType=0x7 1897 | 1898 | DataType=0x0006 1899 | 1900 | LowLimit= 1901 | 1902 | HighLimit= 1903 | 1904 | AccessType=ro 1905 | 1906 | DefaultValue= 1907 | 1908 | PDOMapping=1 1909 | 1910 | ObjFlags=0x0 1911 | 1912 | 1913 | 1914 | [3010sub7] 1915 | 1916 | ParameterName=act_value 6 1917 | 1918 | ObjectType=0x7 1919 | 1920 | DataType=0x0006 1921 | 1922 | LowLimit= 1923 | 1924 | HighLimit= 1925 | 1926 | AccessType=ro 1927 | 1928 | DefaultValue= 1929 | 1930 | PDOMapping=1 1931 | 1932 | ObjFlags=0x0 1933 | 1934 | 1935 | 1936 | [3010sub8] 1937 | 1938 | ParameterName=act_value 7 1939 | 1940 | ObjectType=0x7 1941 | 1942 | DataType=0x0006 1943 | 1944 | LowLimit= 1945 | 1946 | HighLimit= 1947 | 1948 | AccessType=ro 1949 | 1950 | DefaultValue= 1951 | 1952 | PDOMapping=1 1953 | 1954 | ObjFlags=0x0 1955 | ` 1956 | 1957 | func TestDicEDSParse(t *testing.T) { 1958 | // Parse file 1959 | dic, err := DicEDSParse([]byte(TestEDSFile)) 1960 | if err != nil { 1961 | t.Fatal(err) 1962 | } 1963 | 1964 | t.Log(dic) 1965 | } 1966 | -------------------------------------------------------------------------------- /dic_object.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | type DicObject interface { 4 | // For DicRecord and DicArray 5 | 6 | GetIndex() uint16 7 | GetSubIndex() uint8 8 | GetName() string 9 | AddMember(DicObject) 10 | FindIndex(uint16) DicObject 11 | FindName(string) DicObject 12 | 13 | // For DicVariable only 14 | 15 | GetDataType() byte 16 | GetDataLen() int 17 | 18 | SetSize(int) 19 | SetOffset(int) 20 | GetOffset() int 21 | 22 | Read() error 23 | Save() error 24 | 25 | GetData() []byte 26 | SetData([]byte) 27 | 28 | GetStringVal() *string 29 | GetFloatVal() *float64 30 | GetUintVal() *uint64 31 | GetIntVal() *int64 32 | GetBoolVal() *bool 33 | GetByteVal() *byte 34 | 35 | SetStringVal(string) 36 | SetFloatVal(float64) 37 | SetUintVal(uint64) 38 | SetIntVal(int64) 39 | SetBoolVal(bool) 40 | SetByteVal(byte) 41 | 42 | // For All DicObject 43 | 44 | IsDicVariable() bool 45 | SetSDO(*SDOClient) 46 | } 47 | -------------------------------------------------------------------------------- /dic_object_dic.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | type DicObjectDic struct { 4 | Baudrate int 5 | NodeID int 6 | 7 | // Map of Object ids to objects 8 | Indexes map[uint16]DicObject 9 | 10 | // Index to map objects names to objects indexs 11 | NamesIndex map[string]uint16 12 | } 13 | 14 | func NewDicObjectDic() *DicObjectDic { 15 | return &DicObjectDic{ 16 | Indexes: map[uint16]DicObject{}, 17 | NamesIndex: map[string]uint16{}, 18 | } 19 | } 20 | 21 | func (objectDic *DicObjectDic) AddObject(object DicObject) { 22 | objectDic.Indexes[object.GetIndex()] = object 23 | objectDic.NamesIndex[object.GetName()] = object.GetIndex() 24 | } 25 | 26 | func (objectDic *DicObjectDic) FindIndex(index uint16) DicObject { 27 | if object, ok := objectDic.Indexes[index]; ok { 28 | return object 29 | } 30 | 31 | return nil 32 | } 33 | 34 | func (objectDic *DicObjectDic) FindName(name string) DicObject { 35 | if index, ok := objectDic.NamesIndex[name]; ok { 36 | return objectDic.Indexes[index] 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /dic_parser.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | func DicMustParse(a *DicObjectDic, err error) *DicObjectDic { 4 | if err != nil { 5 | panic(err) 6 | } 7 | 8 | return a 9 | } 10 | -------------------------------------------------------------------------------- /dic_record.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | type DicRecord struct { 4 | Description string 5 | Index uint16 6 | Name string 7 | 8 | SDOClient *SDOClient 9 | 10 | SubIndexes map[uint8]DicObject 11 | SubNames map[string]uint8 12 | } 13 | 14 | // GetIndex of DicRecord 15 | func (record *DicRecord) GetIndex() uint16 { 16 | return record.Index 17 | } 18 | 19 | // GetSubIndex not applicable 20 | func (record *DicRecord) GetSubIndex() uint8 { return 0 } 21 | 22 | // GetName of DicRecord 23 | func (record *DicRecord) GetName() string { 24 | return record.Name 25 | } 26 | 27 | // AddMember to DicRecord 28 | func (record *DicRecord) AddMember(object DicObject) { 29 | if record.SubIndexes == nil { 30 | record.SubIndexes = map[uint8]DicObject{} 31 | } 32 | 33 | if record.SubNames == nil { 34 | record.SubNames = map[string]uint8{} 35 | } 36 | 37 | record.SubIndexes[object.GetSubIndex()] = object 38 | record.SubNames[object.GetName()] = object.GetSubIndex() 39 | } 40 | 41 | // FindIndex find by index a DicObject in DicRecord 42 | func (record *DicRecord) FindIndex(index uint16) DicObject { 43 | if object, ok := record.SubIndexes[uint8(index)]; ok { 44 | object.SetSDO(record.SDOClient) 45 | return object 46 | } 47 | 48 | return nil 49 | } 50 | 51 | // FindName find by name a DicObject in DicRecord 52 | func (record *DicRecord) FindName(name string) DicObject { 53 | if index, ok := record.SubNames[name]; ok { 54 | return record.FindIndex(uint16(index)) 55 | } 56 | 57 | return nil 58 | } 59 | 60 | func (record *DicRecord) GetDataType() byte { return 0x00 } 61 | func (record *DicRecord) GetDataLen() int { return 0 } 62 | func (record *DicRecord) SetSize(s int) {} 63 | func (record *DicRecord) SetOffset(s int) {} 64 | func (record *DicRecord) GetOffset() int { return 0 } 65 | func (record *DicRecord) Read() error { return nil } 66 | func (record *DicRecord) Save() error { return nil } 67 | func (record *DicRecord) GetData() []byte { return nil } 68 | func (record *DicRecord) SetData(data []byte) {} 69 | func (record *DicRecord) GetStringVal() *string { return nil } 70 | func (record *DicRecord) GetFloatVal() *float64 { return nil } 71 | func (record *DicRecord) GetUintVal() *uint64 { return nil } 72 | func (record *DicRecord) GetIntVal() *int64 { return nil } 73 | func (record *DicRecord) GetBoolVal() *bool { return nil } 74 | func (record *DicRecord) GetByteVal() *byte { return nil } 75 | func (record *DicRecord) SetStringVal(a string) {} 76 | func (record *DicRecord) SetFloatVal(a float64) {} 77 | func (record *DicRecord) SetUintVal(a uint64) {} 78 | func (record *DicRecord) SetIntVal(a int64) {} 79 | func (record *DicRecord) SetBoolVal(a bool) {} 80 | func (record *DicRecord) SetByteVal(a byte) {} 81 | func (record *DicRecord) IsDicVariable() bool { return false } 82 | 83 | // SetSDO to DicRecord 84 | func (record *DicRecord) SetSDO(sdo *SDOClient) { 85 | record.SDOClient = sdo 86 | } 87 | -------------------------------------------------------------------------------- /dic_types.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "github.com/angelodlfrtr/go-canopen/utils" 5 | ) 6 | 7 | const ( 8 | DicVar byte = 0x07 9 | DicArr byte = 0x08 10 | DicRec byte = 0x09 11 | ) 12 | 13 | const ( 14 | Boolean byte = 0x1 15 | Integer8 byte = 0x2 16 | Integer16 byte = 0x3 17 | Integer32 byte = 0x4 18 | Integer64 byte = 0x15 19 | Unsigned8 byte = 0x5 20 | Unsigned16 byte = 0x6 21 | Unsigned32 byte = 0x7 22 | Unsigned64 byte = 0x1b 23 | 24 | Real32 byte = 0x8 25 | Real64 byte = 0x11 26 | 27 | VisibleString byte = 0x9 28 | OctetString byte = 0xa 29 | UnicodeString byte = 0xb 30 | Domain byte = 0xf 31 | ) 32 | 33 | func IsSignedType(t byte) bool { 34 | return utils.ContainsByte([]byte{ 35 | Integer8, 36 | Integer16, 37 | Integer32, 38 | Integer64, 39 | }, t) 40 | } 41 | 42 | func IsUnsignedType(t byte) bool { 43 | return utils.ContainsByte([]byte{ 44 | Unsigned8, 45 | Unsigned16, 46 | Unsigned32, 47 | Unsigned64, 48 | }, t) 49 | } 50 | 51 | func IsIntegerType(t byte) bool { 52 | return utils.ContainsByte([]byte{ 53 | Unsigned8, 54 | Unsigned16, 55 | Unsigned32, 56 | Unsigned64, 57 | Integer8, 58 | Integer16, 59 | Integer32, 60 | Integer64, 61 | }, t) 62 | } 63 | 64 | func IsFloatType(t byte) bool { 65 | return utils.ContainsByte([]byte{ 66 | Real32, 67 | Real64, 68 | }, t) 69 | } 70 | 71 | func IsNumberType(t byte) bool { 72 | return utils.ContainsByte([]byte{ 73 | Unsigned8, 74 | Unsigned16, 75 | Unsigned32, 76 | Unsigned64, 77 | Integer8, 78 | Integer16, 79 | Integer32, 80 | Integer64, 81 | Real32, 82 | Real64, 83 | }, t) 84 | } 85 | 86 | func IsStringType(t byte) bool { 87 | return utils.ContainsByte([]byte{ 88 | VisibleString, 89 | OctetString, 90 | UnicodeString, 91 | }, t) 92 | } 93 | 94 | func IsDataType(t byte) bool { 95 | return utils.ContainsByte([]byte{ 96 | VisibleString, 97 | OctetString, 98 | UnicodeString, 99 | Domain, 100 | }, t) 101 | } 102 | -------------------------------------------------------------------------------- /dic_variable.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "encoding/ascii85" 5 | "encoding/binary" 6 | "errors" 7 | "unicode/utf16" 8 | ) 9 | 10 | type DicVariable struct { 11 | Unit string 12 | Factor int 13 | Min int 14 | Max int 15 | Default []byte 16 | DataType byte 17 | AccessType string 18 | Description string 19 | 20 | SDOClient *SDOClient 21 | 22 | Data []byte 23 | Offset int 24 | Size int 25 | 26 | Index uint16 27 | SubIndex uint8 28 | Name string 29 | 30 | ValueDescriptions map[string]string 31 | BitDefinitions map[string][]byte 32 | } 33 | 34 | func (variable *DicVariable) GetIndex() uint16 { 35 | return variable.Index 36 | } 37 | 38 | func (variable *DicVariable) GetSubIndex() uint8 { 39 | return variable.SubIndex 40 | } 41 | 42 | func (variable *DicVariable) GetName() string { 43 | return variable.Name 44 | } 45 | 46 | func (variable *DicVariable) AddMember(object DicObject) { 47 | // Do nothing here 48 | } 49 | 50 | func (variable *DicVariable) FindIndex(index uint16) DicObject { 51 | // Do nothing here 52 | return nil 53 | } 54 | 55 | func (variable *DicVariable) FindName(name string) DicObject { 56 | // Do nothing here 57 | return nil 58 | } 59 | 60 | func (variable *DicVariable) SetSDO(sdo *SDOClient) { 61 | variable.SDOClient = sdo 62 | } 63 | 64 | func (variable *DicVariable) IsDicVariable() bool { 65 | return true 66 | } 67 | 68 | func (variable *DicVariable) GetDataType() byte { 69 | return variable.DataType 70 | } 71 | 72 | func (variable *DicVariable) GetDataLen() int { 73 | l := 1 74 | 75 | if variable.DataType == Boolean { 76 | l = 1 77 | } 78 | 79 | if variable.DataType == Integer8 { 80 | l = 1 81 | } 82 | 83 | if variable.DataType == Integer16 { 84 | l = 2 85 | } 86 | 87 | if variable.DataType == Integer32 { 88 | l = 4 89 | } 90 | 91 | if variable.DataType == Integer64 { 92 | l = 8 93 | } 94 | 95 | if variable.DataType == Unsigned8 { 96 | l = 1 97 | } 98 | 99 | if variable.DataType == Unsigned16 { 100 | l = 2 101 | } 102 | 103 | if variable.DataType == Unsigned32 { 104 | l = 4 105 | } 106 | 107 | if variable.DataType == Unsigned64 { 108 | l = 8 109 | } 110 | 111 | if variable.DataType == Real32 { 112 | l = 4 113 | } 114 | 115 | if variable.DataType == Real64 { 116 | l = 8 117 | } 118 | 119 | return l * 8 120 | } 121 | 122 | func (variable *DicVariable) SetSize(s int) { 123 | variable.Size = s 124 | } 125 | 126 | func (variable *DicVariable) SetOffset(s int) { 127 | variable.Offset = s 128 | } 129 | 130 | func (variable *DicVariable) GetOffset() int { 131 | return variable.Offset 132 | } 133 | 134 | func (variable *DicVariable) AddValueDescription(name string, des string) { 135 | variable.ValueDescriptions[name] = des 136 | } 137 | 138 | func (variable *DicVariable) AddBitDefinition(name string, bits []byte) { 139 | variable.BitDefinitions[name] = bits 140 | } 141 | 142 | // Read variable value using SDO 143 | func (variable *DicVariable) Read() error { 144 | if variable.SDOClient == nil { 145 | return errors.New("SDOClient required") 146 | } 147 | 148 | data, err := variable.SDOClient.Read(variable.Index, variable.SubIndex) 149 | if err != nil { 150 | return err 151 | } 152 | 153 | variable.Data = data 154 | 155 | return nil 156 | } 157 | 158 | // Write variable value using SDO 159 | func (variable *DicVariable) Write(data []byte) error { 160 | if variable.SDOClient == nil { 161 | return errors.New("SDOClient required") 162 | } 163 | 164 | return variable.SDOClient.Write( 165 | variable.Index, 166 | variable.SubIndex, 167 | variable.IsDomainDataType(), 168 | variable.Data, 169 | ) 170 | } 171 | 172 | // Save variable.Data using SDO 173 | func (variable *DicVariable) Save() error { 174 | return variable.Write(variable.Data) 175 | } 176 | 177 | func (variable *DicVariable) IsDomainDataType() bool { 178 | return variable.DataType == Domain 179 | } 180 | 181 | func (variable *DicVariable) GetData() []byte { 182 | return variable.Data 183 | } 184 | 185 | func (variable *DicVariable) SetData(data []byte) { 186 | variable.Data = data 187 | } 188 | 189 | func (variable *DicVariable) GetStringVal() *string { 190 | if !IsStringType(variable.DataType) { 191 | return nil 192 | } 193 | 194 | // @TODO: check if all working 195 | var v string 196 | 197 | if variable.DataType == VisibleString { 198 | dst := []byte{} 199 | ascii85.Decode(dst, variable.Data, false) 200 | v = string(dst) 201 | } 202 | 203 | if variable.DataType == UnicodeString { 204 | src := make([]uint16, len(variable.Data)) 205 | for i, r := range variable.Data { 206 | src[i] = uint16(r) 207 | } 208 | dst := utf16.Decode(src) 209 | v = string(dst) 210 | } 211 | 212 | if len(v) == 0 { 213 | v = string(variable.Data) 214 | } 215 | 216 | return &v 217 | } 218 | 219 | func (variable *DicVariable) GetFloatVal() *float64 { 220 | if !IsFloatType(variable.DataType) { 221 | return nil 222 | } 223 | 224 | var v float64 225 | 226 | // @TODO 227 | 228 | return &v 229 | } 230 | 231 | func (variable *DicVariable) GetUintVal() *uint64 { 232 | if !IsUnsignedType(variable.DataType) { 233 | return nil 234 | } 235 | 236 | var v uint64 237 | 238 | if variable.DataType == Unsigned8 { 239 | v = uint64(variable.Data[0]) 240 | } 241 | 242 | if variable.DataType == Unsigned16 { 243 | v = uint64(binary.LittleEndian.Uint16(variable.Data)) 244 | } 245 | 246 | if variable.DataType == Unsigned32 { 247 | v = uint64(binary.LittleEndian.Uint32(variable.Data)) 248 | } 249 | 250 | if variable.DataType == Unsigned64 { 251 | v = binary.LittleEndian.Uint64(variable.Data) 252 | } 253 | 254 | return &v 255 | } 256 | 257 | func (variable *DicVariable) GetIntVal() *int64 { 258 | if !IsSignedType(variable.DataType) { 259 | return nil 260 | } 261 | 262 | var v int64 263 | 264 | if variable.DataType == Integer8 { 265 | v = int64(int8(variable.Data[0])) 266 | } 267 | 268 | // @TODO: https://groups.google.com/forum/#!topic/golang-nuts/f1QQkP19G9Q 269 | if variable.DataType == Integer16 { 270 | v = int64(binary.LittleEndian.Uint16(variable.Data)) 271 | } 272 | 273 | if variable.DataType == Integer32 { 274 | v = int64(binary.LittleEndian.Uint32(variable.Data)) 275 | } 276 | 277 | if variable.DataType == Integer64 { 278 | v = int64(binary.LittleEndian.Uint64(variable.Data)) 279 | } 280 | 281 | return &v 282 | } 283 | 284 | func (variable *DicVariable) GetBoolVal() *bool { 285 | if variable.DataType != Boolean { 286 | return nil 287 | } 288 | 289 | v := false 290 | 291 | // @TODO 292 | 293 | return &v 294 | } 295 | 296 | func (variable *DicVariable) GetByteVal() *byte { 297 | var v byte 298 | 299 | if variable.DataType == Unsigned8 { 300 | v = variable.Data[0] 301 | } 302 | 303 | return &v 304 | } 305 | 306 | func (variable *DicVariable) SetStringVal(a string) { 307 | // @TODO 308 | } 309 | 310 | func (variable *DicVariable) SetFloatVal(a float64) { 311 | // @TODO 312 | } 313 | 314 | func (variable *DicVariable) SetUintVal(a uint64) { 315 | if !IsUnsignedType(variable.DataType) { 316 | return 317 | } 318 | 319 | if variable.DataType == Unsigned8 { 320 | variable.Data[0] = byte(a) 321 | } 322 | 323 | if variable.DataType == Unsigned16 { 324 | binary.LittleEndian.PutUint16(variable.Data, uint16(a)) 325 | } 326 | 327 | if variable.DataType == Unsigned32 { 328 | binary.LittleEndian.PutUint32(variable.Data, uint32(a)) 329 | } 330 | 331 | if variable.DataType == Unsigned64 { 332 | binary.LittleEndian.PutUint64(variable.Data, uint64(a)) 333 | } 334 | } 335 | 336 | func (variable *DicVariable) SetIntVal(a int64) { 337 | if !IsSignedType(variable.DataType) { 338 | return 339 | } 340 | 341 | if variable.DataType == Integer8 { 342 | variable.Data[0] = byte(a) 343 | } 344 | 345 | // @TODO: https://groups.google.com/forum/#!topic/golang-nuts/f1QQkP19G9Q 346 | if variable.DataType == Integer16 { 347 | binary.LittleEndian.PutUint16(variable.Data, uint16(a)) 348 | } 349 | 350 | if variable.DataType == Integer32 { 351 | binary.LittleEndian.PutUint32(variable.Data, uint32(a)) 352 | } 353 | 354 | if variable.DataType == Integer64 { 355 | binary.LittleEndian.PutUint64(variable.Data, uint64(a)) 356 | } 357 | } 358 | 359 | func (variable *DicVariable) SetBoolVal(a bool) { 360 | if variable.DataType == Boolean { 361 | if a { 362 | variable.Data[0] = 0x01 363 | } else { 364 | variable.Data[0] = 0x02 365 | } 366 | } 367 | } 368 | 369 | func (variable *DicVariable) SetByteVal(a byte) { 370 | if variable.DataType == Unsigned8 { 371 | variable.Data[0] = a 372 | } 373 | } 374 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/angelodlfrtr/go-canopen 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/angelodlfrtr/go-can v0.0.4 7 | github.com/google/uuid v1.3.0 8 | gopkg.in/ini.v1 v1.66.4 9 | ) 10 | 11 | require ( 12 | github.com/angelodlfrtr/serial v0.0.0-20190912094943-d028474db63c // indirect 13 | github.com/brutella/can v0.0.1 // indirect 14 | github.com/stretchr/testify v1.7.0 // indirect 15 | golang.org/x/sys v0.1.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/angelodlfrtr/go-can v0.0.4 h1:/Wj3ZQ2YWG2dfoHqZHchfP2sZ/UccbrEBTV0nj/HRKo= 2 | github.com/angelodlfrtr/go-can v0.0.4/go.mod h1:e9NV8485R6+MwVHY1424SxwpCzsIRYd6I8noHvgHzQQ= 3 | github.com/angelodlfrtr/serial v0.0.0-20190912094943-d028474db63c h1:Q5NYSNKJn3QdfE4q7yG6m+r0OLFrsC94KDQPGqj5OkE= 4 | github.com/angelodlfrtr/serial v0.0.0-20190912094943-d028474db63c/go.mod h1:kGJNzwu4M6uVHZPXuE7m6+f3BvYrhFH76a90n69CxEk= 5 | github.com/brutella/can v0.0.1 h1:Rz+2Zuje3NT79daon8wPN9+VphH3/kl1DP8Dhf/k1NI= 6 | github.com/brutella/can v0.0.1/go.mod h1:NYDxbQito3w4+4DcjWs/fpQ3xyaFdpXw/KYqtZFU98k= 7 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 8 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 14 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 15 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | golang.org/x/sys v0.0.0-20181213200352-4d1cda033e06/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 17 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 18 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 19 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 20 | gopkg.in/ini.v1 v1.66.4 h1:SsAcf+mM7mRZo2nJNGt8mZCjG8ZRaNGMURJw7BsIST4= 21 | gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 22 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 23 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 24 | -------------------------------------------------------------------------------- /network.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "sync" 7 | "time" 8 | 9 | "github.com/angelodlfrtr/go-can" 10 | "github.com/angelodlfrtr/go-canopen/utils" 11 | "github.com/google/uuid" 12 | ) 13 | 14 | // Network represent the global nodes network 15 | type Network struct { 16 | // mutex for FramesChans access 17 | sync.Mutex 18 | 19 | // Bus is the go-can bus 20 | Bus can.Bus 21 | 22 | // Nodes contain the network nodes 23 | Nodes map[int]*Node 24 | 25 | // FramesChans contains a list of chan when is sent each frames from network bus. 26 | FramesChans []*NetworkFramesChan 27 | 28 | // NMTMaster contain nmt control struct 29 | NMTMaster *NMTMaster 30 | 31 | // stopChan permit to stop network 32 | stopChan chan bool 33 | 34 | // running is network running 35 | running bool 36 | 37 | // BusReadErrChan @TODO on go-can 38 | BusReadErrChan chan error 39 | } 40 | 41 | // NewNetwork a new Network with given bus 42 | func NewNetwork(bus can.Bus) (*Network, error) { 43 | // Create network 44 | netw := &Network{Bus: bus} 45 | 46 | // Set nmt and listen for nmt hearbeat messages 47 | netw.NMTMaster = NewNMTMaster(0, netw) 48 | 49 | return netw, nil 50 | } 51 | 52 | // Run listen handlers for frames on bus 53 | func (network *Network) Run() error { 54 | if network.running { 55 | return nil 56 | } 57 | 58 | network.running = true 59 | network.stopChan = make(chan bool, 1) 60 | 61 | // Start network nmt master hearbeat listener 62 | if err := network.NMTMaster.ListenForHeartbeat(); err != nil { 63 | return err 64 | } 65 | 66 | go func() { 67 | for { 68 | select { 69 | case <-network.stopChan: 70 | // Stop goroutine 71 | return 72 | case frm := <-network.Bus.ReadChan(): 73 | network.Lock() 74 | 75 | // Send frame to frames chans 76 | for _, ch := range network.FramesChans { 77 | ch.Publish(frm) 78 | } 79 | 80 | network.Unlock() 81 | } 82 | } 83 | }() 84 | 85 | return nil 86 | } 87 | 88 | // Stop handlers for frames on bus 89 | func (network *Network) Stop() error { 90 | if !network.running { 91 | return nil 92 | } 93 | 94 | // Start network nmt master hearbeat listener 95 | if err := network.NMTMaster.UnlistenForHeartbeat(); err != nil { 96 | return err 97 | } 98 | 99 | // Stop each nodes 100 | for _, node := range network.Nodes { 101 | node.Stop() 102 | } 103 | 104 | network.stopChan <- true 105 | 106 | return nil 107 | } 108 | 109 | // Send a frame on network 110 | func (network *Network) Send(arbID uint32, data []byte) error { 111 | frm := &can.Frame{ 112 | ArbitrationID: arbID, 113 | DLC: uint8(len(data)), 114 | } 115 | 116 | if len(data) > 8 { 117 | frm.DLC = uint8(8) 118 | } 119 | 120 | // Copy data to 8 byte array 121 | var arr [8]byte 122 | copy(arr[0:], data[:int(frm.DLC)]) 123 | 124 | // Set data in frame 125 | frm.Data = arr 126 | 127 | // Write frame to port 128 | return network.Bus.Write(frm) 129 | } 130 | 131 | // AddNode add a node to the network 132 | func (network *Network) AddNode(node *Node, objectDic *DicObjectDic, uploadEDS bool) *Node { 133 | if uploadEDS { 134 | // @TODO: download definition from node if true 135 | log.Fatal("uploading EDS not supported for now") 136 | } 137 | 138 | if node == nil { 139 | log.Fatal("Cannot use nil Node") 140 | return nil 141 | } 142 | 143 | // Set node network 144 | node.SetNetwork(network) 145 | 146 | // Set ObjectDic 147 | node.SetObjectDic(objectDic) 148 | 149 | // Set nmt and listen for nmt hearbeat messages 150 | node.NMTMaster = NewNMTMaster(node.ID, network) 151 | 152 | // Init node 153 | node.Init() 154 | 155 | // Start nmt master hearbeat listener 156 | if err := node.NMTMaster.ListenForHeartbeat(); err != nil { 157 | log.Fatalf("Failed to start nmt master on node %d with err %v", node.ID, err) 158 | } 159 | 160 | network.Lock() 161 | defer network.Unlock() 162 | // Initialize Nodes 163 | if network.Nodes == nil { 164 | network.Nodes = map[int]*Node{} 165 | } 166 | 167 | // Append node to network 168 | network.Nodes[node.ID] = node 169 | 170 | return node 171 | } 172 | 173 | // GetNode by node id. Return error if node dont exist in network.Nodes 174 | func (network *Network) GetNode(nodeID int) (*Node, error) { 175 | network.Lock() 176 | defer network.Unlock() 177 | 178 | if node, ok := network.Nodes[nodeID]; ok { 179 | return node, nil 180 | } 181 | 182 | return nil, fmt.Errorf("no node with id %d", nodeID) 183 | } 184 | 185 | // AcquireFramesChan create a new FrameChan 186 | func (network *Network) AcquireFramesChan(filterFunc networkFramesChanFilterFunc) *NetworkFramesChan { 187 | network.Lock() 188 | defer network.Unlock() 189 | 190 | // Create frame chan 191 | chanID := uuid.Must(uuid.NewRandom()).String() 192 | frameChan := &NetworkFramesChan{ 193 | ID: chanID, 194 | Filter: filterFunc, 195 | C: make(chan *can.Frame), 196 | } 197 | 198 | // Append network.FramesChans 199 | network.FramesChans = append(network.FramesChans, frameChan) 200 | 201 | return frameChan 202 | } 203 | 204 | // ReleaseFramesChan release (close) a FrameChan 205 | func (network *Network) ReleaseFramesChan(id string) { 206 | network.Lock() 207 | defer network.Unlock() 208 | 209 | var framesChan *NetworkFramesChan 210 | var framesChanIndex *int 211 | 212 | for idx, fc := range network.FramesChans { 213 | if fc.ID == id { 214 | framesChan = fc 215 | idxx := idx 216 | framesChanIndex = &idxx 217 | break 218 | } 219 | } 220 | 221 | if framesChanIndex == nil { 222 | return 223 | } 224 | 225 | // Close chan 226 | close(framesChan.C) 227 | 228 | // Remove frameChan from network.FramesChans 229 | network.FramesChans = append( 230 | network.FramesChans[:*framesChanIndex], 231 | network.FramesChans[*framesChanIndex+1:]..., 232 | ) 233 | } 234 | 235 | // Search send data to network and wait for nodes response 236 | func (network *Network) Search(limit int, timeout time.Duration) ([]*Node, error) { 237 | if limit == 0 { 238 | limit = 127 239 | } 240 | 241 | // Canopen service 242 | services := []uint32{0x700, 0x580, 0x180, 0x280, 0x380, 0x480, 0x80} 243 | 244 | // Listen messages 245 | framesChan := network.AcquireFramesChan(nil) 246 | 247 | // Nodes found 248 | nodes := make([]*Node, 0, limit) 249 | 250 | go func() { 251 | for frm := range framesChan.C { 252 | service := frm.ArbitrationID & 0x780 253 | nodeID := int(frm.ArbitrationID & 0x7F) 254 | 255 | if nodeID != 0 { 256 | if utils.ContainsUint32(services, service) { 257 | // Append only if not already exist in nodes slice 258 | nodeExist := false 259 | for _, n := range nodes { 260 | if n.ID == nodeID { 261 | nodeExist = true 262 | break 263 | } 264 | } 265 | 266 | if nodeExist { 267 | continue 268 | } 269 | 270 | nNode := NewNode(nodeID, nil, nil) 271 | nodes = append(nodes, nNode) 272 | } 273 | } 274 | } 275 | }() 276 | 277 | // Send ping for `limit` nodes 278 | reqData := []byte{0x40, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00} 279 | for i := 1; i <= limit; i++ { 280 | if err := network.Send(uint32(0x600+i), reqData); err != nil { 281 | return nil, err 282 | } 283 | 284 | time.Sleep(1 * time.Millisecond) 285 | } 286 | 287 | // timer := time.NewTimer(timeout) 288 | time.Sleep(timeout) 289 | 290 | // Release fram chan (will stop goroutine) 291 | network.ReleaseFramesChan(framesChan.ID) 292 | 293 | // Return nodes 294 | return nodes, nil 295 | } 296 | -------------------------------------------------------------------------------- /network_frames_chan.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "github.com/angelodlfrtr/go-can" 5 | ) 6 | 7 | type networkFramesChanFilterFunc *(func(*can.Frame) bool) 8 | 9 | // NetworkFramesChan contain a Chan, and ID and a Filter function 10 | // Each FrameChan can have a filter function which return a boolean, 11 | // and for each frame, the filter func is called. If func return true, the frame is returned, 12 | // else dont send frame. 13 | type NetworkFramesChan struct { 14 | ID string 15 | C chan *can.Frame 16 | Filter networkFramesChanFilterFunc 17 | } 18 | 19 | func (networkFramesCh *NetworkFramesChan) Publish(frm *can.Frame) { 20 | if networkFramesCh.Filter != nil { 21 | if (*networkFramesCh.Filter)(frm) { 22 | select { 23 | case networkFramesCh.C <- frm: 24 | default: 25 | } 26 | } 27 | 28 | return 29 | } 30 | 31 | select { 32 | case networkFramesCh.C <- frm: 33 | default: 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /network_test.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "sync" 8 | "testing" 9 | "time" 10 | 11 | "github.com/angelodlfrtr/go-can" 12 | "github.com/angelodlfrtr/go-can/transports" 13 | ) 14 | 15 | func getTestPort() string { 16 | if a := os.Getenv("CAN_TEST_PORT"); len(a) > 0 { 17 | return a 18 | } 19 | 20 | return "/dev/tty.usbserial-1420" 21 | } 22 | 23 | func getNetwork() (*Network, error) { 24 | testPort := getTestPort() 25 | transport := &transports.USBCanAnalyzer{ 26 | Port: testPort, 27 | BaudRate: 2000000, 28 | } 29 | 30 | bus := can.Bus{Transport: transport} 31 | 32 | if err := bus.Open(); err != nil { 33 | return nil, err 34 | } 35 | 36 | netw, err := NewNetwork(bus) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | if err := netw.Run(); err != nil { 42 | return nil, err 43 | } 44 | 45 | return netw, nil 46 | } 47 | 48 | func searchNodes() ([]*Node, error) { 49 | network, err := getNetwork() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | // Run search (in my case), node ids a returned after ~~500ms 55 | // So be secure with timeout 56 | timeout := 1 * time.Second 57 | nodes, err := network.Search(127, timeout) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | return nodes, nil 63 | } 64 | 65 | func TestSend(t *testing.T) { 66 | network, err := getNetwork() 67 | if err != nil { 68 | t.Fatal(err) 69 | } 70 | 71 | err = network.Send(uint32(0x01), []byte{0x0, 0x0, 0x0}) 72 | 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | } 77 | 78 | func TestSearch(t *testing.T) { 79 | nodes, err := searchNodes() 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | 84 | // Expect a least one node in results 85 | if len(nodes) == 0 { 86 | t.Fatal("No nodes found") 87 | } 88 | 89 | t.Log(nodes) 90 | } 91 | 92 | func TestAddNode(t *testing.T) { 93 | network := &Network{} 94 | node := &Node{ID: 1} 95 | network.AddNode(node, nil, false) 96 | 97 | if len(network.Nodes) != 1 { 98 | t.Fatal("Invalid network.Nodes len") 99 | } 100 | } 101 | 102 | func TestGetNode(t *testing.T) { 103 | network := &Network{} 104 | node := &Node{ID: 1} 105 | network.AddNode(node, nil, false) 106 | 107 | if len(network.Nodes) != 1 { 108 | t.Fatal("Invalid network.Nodes len") 109 | } 110 | 111 | nodeGot, err := network.GetNode(node.ID) 112 | if err != nil { 113 | t.Fatal(err) 114 | } 115 | 116 | if nodeGot == nil { 117 | t.Fatal("Node not found") 118 | } 119 | } 120 | 121 | func TestAll(t *testing.T) { 122 | testPort := getTestPort() 123 | transport := &transports.USBCanAnalyzer{ 124 | Port: testPort, 125 | BaudRate: 2000000, 126 | } 127 | 128 | bus := can.Bus{Transport: transport} 129 | 130 | if err := bus.Open(); err != nil { 131 | t.Fatal(err) 132 | } 133 | 134 | network, err := NewNetwork(bus) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | 139 | if err := network.Run(); err != nil { 140 | t.Fatal(err) 141 | } 142 | 143 | // Load object dic 144 | objectDicFilePath := os.Getenv("CAN_TEST_EDS") 145 | if len(objectDicFilePath) == 0 { 146 | t.Fatal("Invalid object dic file path") 147 | } 148 | 149 | // Run search node ids a returned after ~500ms in my case 150 | // So be secure with timeout 151 | searchTimeout := time.Duration(5) * time.Second 152 | nodes, err := network.Search(256, searchTimeout) 153 | if err != nil { 154 | t.Fatal(err) 155 | } 156 | 157 | if len(nodes) == 0 { 158 | t.Fatal("No nodes found") 159 | } 160 | 161 | fmt.Println("Nodes found", len(nodes)) 162 | 163 | var wg sync.WaitGroup 164 | for _, n := range nodes { 165 | wg.Add(1) 166 | 167 | go func(node *Node) { 168 | // Parse eds file 169 | dic := DicMustParse(DicEDSParse(objectDicFilePath)) 170 | 171 | network.AddNode(node, dic, false) 172 | 173 | fmt.Println("Reading PDO") 174 | 175 | if err := node.PDONode.Read(); err != nil { 176 | log.Fatal(err) 177 | } 178 | 179 | // node := nodes[0] 180 | // node, _ := network.GetNode(4) 181 | t.Log("PDO NODE readed ID", node.ID) 182 | 183 | wg.Done() 184 | }(n) 185 | } 186 | wg.Wait() 187 | 188 | fmt.Println("Done") 189 | 190 | // Stop network 191 | network.Stop() 192 | fmt.Println("Network stopped") 193 | 194 | // Stop bus 195 | bus.Close() 196 | fmt.Println("Bus closed") 197 | } 198 | 199 | func TestReboot(t *testing.T) { 200 | testPort := getTestPort() 201 | transport := &transports.USBCanAnalyzer{ 202 | Port: testPort, 203 | BaudRate: 2000000, 204 | } 205 | 206 | bus := can.Bus{Transport: transport} 207 | 208 | if err := bus.Open(); err != nil { 209 | t.Fatal(err) 210 | } 211 | 212 | network, err := NewNetwork(bus) 213 | if err != nil { 214 | t.Fatal(err) 215 | } 216 | 217 | if err := network.Run(); err != nil { 218 | t.Fatal(err) 219 | } 220 | 221 | // Load object dic 222 | objectDicFilePath := os.Getenv("CAN_TEST_EDS") 223 | if len(objectDicFilePath) == 0 { 224 | t.Fatal("Invalid object dic file path") 225 | } 226 | 227 | // Run search node ids a returned after ~500ms in my case 228 | // So be secure with timeout 229 | searchTimeout := time.Duration(4) * time.Second 230 | nodes, err := network.Search(256, searchTimeout) 231 | if err != nil { 232 | t.Fatal(err) 233 | } 234 | 235 | if len(nodes) == 0 { 236 | t.Fatal("No nodes found") 237 | } 238 | 239 | fmt.Println("Nodes found", len(nodes)) 240 | 241 | var wg sync.WaitGroup 242 | errChan := make(chan error) 243 | 244 | for _, n := range nodes { 245 | wg.Add(1) 246 | 247 | go func(node *Node) { 248 | // Parse eds file 249 | dic := DicMustParse(DicEDSParse(objectDicFilePath)) 250 | 251 | network.AddNode(node, dic, false) 252 | 253 | fmt.Println("Reading PDO") 254 | 255 | if err := node.PDONode.Read(); err != nil { 256 | select { 257 | case errChan <- err: 258 | default: 259 | } 260 | } 261 | 262 | // node := nodes[0] 263 | // node, _ := network.GetNode(4) 264 | t.Log("PDO NODE readed ID", node.ID) 265 | 266 | wg.Done() 267 | }(n) 268 | } 269 | 270 | wg.Wait() 271 | 272 | select { 273 | case e := <-errChan: 274 | t.Fatal(e) 275 | default: 276 | close(errChan) 277 | } 278 | 279 | // Reboot first node 280 | node := nodes[0] 281 | 282 | fmt.Println("Rebooting") 283 | node.NMTMaster.SetState("RESET") 284 | 285 | done := make(chan bool) 286 | 287 | go func() { 288 | timeout := 20 * time.Second 289 | node.NMTMaster.WaitForBootup(&timeout) 290 | fmt.Println("Rebooted") 291 | done <- true 292 | }() 293 | 294 | <-done 295 | } 296 | -------------------------------------------------------------------------------- /nmt_master.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/angelodlfrtr/go-can" 8 | ) 9 | 10 | var NMTStates = map[int]string{ 11 | 0: "INITIALISING", 12 | 4: "STOPPED", 13 | 5: "OPERATIONAL", 14 | 80: "SLEEP", 15 | 96: "STANDBY", 16 | 127: "PRE-OPERATIONAL", 17 | } 18 | 19 | var NMTCommands = map[string]int{ 20 | "OPERATIONAL": 1, 21 | "STOPPED": 2, 22 | "SLEEP": 80, 23 | "STANDBY": 96, 24 | "PRE-OPERATIONAL": 128, 25 | "INITIALISING": 129, 26 | "RESET": 129, 27 | "RESET COMMUNICATION": 130, 28 | } 29 | 30 | var NMTCommandToState = map[int]int{ 31 | 1: 5, 32 | 2: 4, 33 | 80: 80, 34 | 96: 96, 35 | 128: 127, 36 | 129: 0, 37 | 130: 0, 38 | } 39 | 40 | type NMTMaster struct { 41 | NodeID int 42 | Network *Network 43 | State int 44 | StateReceived *int 45 | Timestamp *time.Time 46 | Listening bool 47 | stopChan chan bool 48 | 49 | // networkFramesChanID is used to store and later close the network frames channel 50 | networkFramesChanID *string 51 | } 52 | 53 | // NewNMTMaster return a new instance of Master 54 | func NewNMTMaster(nodeID int, network *Network) *NMTMaster { 55 | return &NMTMaster{ 56 | NodeID: nodeID, 57 | Network: network, 58 | stopChan: make(chan bool, 1), 59 | } 60 | } 61 | 62 | // UnlistenForHeartbeat listen message on network 63 | func (master *NMTMaster) UnlistenForHeartbeat() error { 64 | if master.Network == nil { 65 | return errors.New("no network defined") 66 | } 67 | 68 | if master.networkFramesChanID == nil { 69 | return errors.New("not listening") 70 | } 71 | 72 | // Stop listen 73 | master.stopChan <- true 74 | 75 | // Release chan 76 | // The chan stop will have effect to close goroutine launched in ListenForHeartbeat 77 | master.Network.ReleaseFramesChan(*master.networkFramesChanID) 78 | 79 | master.Listening = false 80 | 81 | return nil 82 | } 83 | 84 | // ListenForHeartbeat listen message on network 85 | func (master *NMTMaster) ListenForHeartbeat() error { 86 | if master.Network == nil { 87 | return errors.New("no network defined") 88 | } 89 | 90 | // Already listening ? 91 | if master.Listening { 92 | return nil 93 | } 94 | 95 | master.Listening = true 96 | 97 | // Hearbeat message arbID 98 | eventName := 0x700 + master.NodeID 99 | 100 | // Filter func for messages on network 101 | filterFunc := func(frm *can.Frame) bool { 102 | return frm.ArbitrationID == uint32(eventName) 103 | } 104 | 105 | // Get frames chan 106 | framesChan := master.Network.AcquireFramesChan(&filterFunc) 107 | master.networkFramesChanID = &framesChan.ID 108 | 109 | // Listen for messages 110 | go func() { 111 | select { 112 | case <-master.stopChan: 113 | // Stop goroutine 114 | return 115 | case frm := <-framesChan.C: 116 | master.handleHeartbeatFrame(frm) 117 | } 118 | }() 119 | 120 | return nil 121 | } 122 | 123 | func (master *NMTMaster) handleHeartbeatFrame(frm *can.Frame) { 124 | now := time.Now() 125 | master.Timestamp = &now 126 | 127 | newState := int(frm.Data[0]) 128 | master.StateReceived = &newState 129 | 130 | if newState == 0 { 131 | master.State = 127 132 | } else { 133 | master.State = newState 134 | } 135 | 136 | // @TODO: emit state 137 | } 138 | 139 | // SendCommand to target node 140 | func (master *NMTMaster) SendCommand(code int) error { 141 | data := []byte{uint8(code), uint8(master.NodeID)} 142 | return master.Network.Send(0, data) 143 | } 144 | 145 | // SetState for target node, and send command 146 | func (master *NMTMaster) SetState(cmd string) error { 147 | if _, ok := NMTCommands[cmd]; !ok { 148 | return errors.New("invalid NMT state") 149 | } 150 | 151 | code := NMTCommands[cmd] 152 | master.StateReceived = nil 153 | 154 | return master.SendCommand(code) 155 | } 156 | 157 | // GetStateString for target node 158 | func (master *NMTMaster) GetStateString() string { 159 | if s, ok := NMTStates[master.State]; ok { 160 | return s 161 | } 162 | 163 | return "" 164 | } 165 | 166 | // WaitForBootup return when the node has *StateReceived == 0 167 | // with a default timeout of 10s 168 | func (master *NMTMaster) WaitForBootup(timeout *time.Duration) error { 169 | if timeout == nil { 170 | tmeout := time.Duration(10) * time.Second 171 | timeout = &tmeout 172 | } 173 | 174 | start := time.Now() 175 | 176 | for { 177 | if time.Since(start) > *timeout { 178 | return errors.New("timeout execeded") 179 | } 180 | 181 | if master.StateReceived != nil { 182 | if *master.StateReceived == 5 { 183 | break 184 | } 185 | } 186 | 187 | time.Sleep(time.Millisecond * 100) 188 | } 189 | 190 | return nil 191 | } 192 | -------------------------------------------------------------------------------- /node.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | // Node is a canopen node 4 | type Node struct { 5 | // Each node has an id, which is ArbitrationID & 0x7F 6 | ID int 7 | 8 | Network *Network 9 | ObjectDic *DicObjectDic 10 | 11 | SDOClient *SDOClient 12 | PDONode *PDONode 13 | NMTMaster *NMTMaster 14 | } 15 | 16 | func NewNode(id int, network *Network, objectDic *DicObjectDic) *Node { 17 | node := &Node{ 18 | ID: id, 19 | Network: network, 20 | ObjectDic: objectDic, 21 | } 22 | 23 | return node 24 | } 25 | 26 | // SetNetwork set node.Network to the desired network 27 | func (node *Node) SetNetwork(network *Network) { 28 | node.Network = network 29 | } 30 | 31 | // SetObjectDic set node.ObjectDic to the desired ObjectDic 32 | func (node *Node) SetObjectDic(objectDic *DicObjectDic) { 33 | node.ObjectDic = objectDic 34 | } 35 | 36 | // Init create sdo clients, pdo nodes, nmt master 37 | func (node *Node) Init() { 38 | node.SDOClient = NewSDOClient(node) 39 | node.PDONode = NewPDONode(node) 40 | node.NMTMaster = NewNMTMaster(node.ID, node.Network) 41 | 42 | // @TODO: list for NMTMaster 43 | // @TODO: implement EMCY 44 | } 45 | 46 | // Stop node 47 | func (node *Node) Stop() { 48 | // Stop nmt master 49 | node.NMTMaster.UnlistenForHeartbeat() 50 | 51 | // Stop pdo listeners 52 | for _, mm := range node.PDONode.RX.Maps { 53 | mm.Unlisten() 54 | } 55 | for _, mm := range node.PDONode.TX.Maps { 56 | mm.Unlisten() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pdo_map.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "sync" 7 | "time" 8 | 9 | "github.com/angelodlfrtr/go-can" 10 | "github.com/google/uuid" 11 | ) 12 | 13 | const ( 14 | MapPDONotValid int64 = 1 << 31 15 | MapRTRNotAllowed int = 1 << 30 16 | ) 17 | 18 | type PDOMapChangeChan struct { 19 | ID string 20 | C chan []byte 21 | } 22 | 23 | type PDOMap struct { 24 | sync.Mutex 25 | 26 | PDONode *PDONode 27 | ComRecord DicObject 28 | MapArray DicObject 29 | 30 | Enabled bool 31 | CobID int 32 | RTRAllowed bool 33 | TransType byte 34 | EventTimer byte 35 | 36 | Map map[int]DicObject 37 | 38 | OldData []byte 39 | Data []byte 40 | 41 | Timestamp *time.Time 42 | Period *time.Duration 43 | 44 | IsReceived bool 45 | 46 | ChangeChans []*PDOMapChangeChan 47 | 48 | listening bool 49 | chanChanStop chan bool 50 | } 51 | 52 | // NewPDOMap return a PDOMap initialized 53 | func NewPDOMap(pdoNode *PDONode, comRecord, mapArray DicObject) *PDOMap { 54 | return &PDOMap{ 55 | PDONode: pdoNode, 56 | ComRecord: comRecord, 57 | MapArray: mapArray, 58 | RTRAllowed: true, 59 | ChangeChans: []*PDOMapChangeChan{}, 60 | } 61 | } 62 | 63 | // FindIndex find a object by index 64 | func (m *PDOMap) FindIndex(idx int) DicObject { 65 | if ma, ok := m.Map[idx]; ok { 66 | return ma 67 | } 68 | 69 | return nil 70 | } 71 | 72 | // FindName find a object by name 73 | func (m *PDOMap) FindName(name string) DicObject { 74 | var r DicObject 75 | 76 | for _, rr := range m.Map { 77 | if rr.GetName() == name { 78 | r = rr 79 | break 80 | } 81 | } 82 | 83 | return r 84 | } 85 | 86 | // GetTotalSize of a map 87 | func (m *PDOMap) GetTotalSize() int { 88 | size := 0 89 | 90 | for _, rr := range m.Map { 91 | size += rr.GetDataLen() 92 | } 93 | 94 | return size 95 | } 96 | 97 | func (m *PDOMap) UpdateDataSize() { 98 | tSize := m.GetTotalSize() 99 | m.Data = make([]byte, 0, tSize) 100 | } 101 | 102 | func (m *PDOMap) SetData(data []byte) { 103 | m.OldData = m.Data 104 | m.Data = data 105 | } 106 | 107 | // Listen for changes on map from network 108 | func (m *PDOMap) Listen() error { 109 | if m.CobID == 0 { 110 | return errors.New("call Read() on this map before listening") 111 | } 112 | 113 | if m.listening { 114 | return nil 115 | } 116 | 117 | m.listening = true 118 | m.chanChanStop = make(chan bool, 1) 119 | 120 | now := time.Now() 121 | m.Timestamp = &now 122 | 123 | filterFunc := func(frm *can.Frame) bool { 124 | return frm.ArbitrationID == uint32(m.CobID) 125 | } 126 | 127 | framesChan := m.PDONode.Node.Network.AcquireFramesChan(&filterFunc) 128 | 129 | go func() { 130 | for { 131 | select { 132 | case <-m.chanChanStop: 133 | // stop goroutine 134 | return 135 | case frm := <-framesChan.C: 136 | m.Lock() 137 | m.IsReceived = true 138 | m.SetData(frm.GetData()) 139 | 140 | // @TODO m.Period = frm.Timestamp - m.Timestamp; 141 | now := time.Now() 142 | m.Timestamp = &now 143 | 144 | // If data changed 145 | if !reflect.DeepEqual(m.OldData, m.Data) { 146 | for _, changeChan := range m.ChangeChans { 147 | select { 148 | case changeChan.C <- m.Data: 149 | default: 150 | } 151 | } 152 | } 153 | 154 | m.Unlock() 155 | } 156 | } 157 | }() 158 | 159 | return nil 160 | } 161 | 162 | // Unlisten for changes on map from network 163 | func (m *PDOMap) Unlisten() { 164 | m.Lock() 165 | defer m.Unlock() 166 | 167 | if !m.listening { 168 | return 169 | } 170 | 171 | m.chanChanStop <- true 172 | close(m.chanChanStop) 173 | 174 | m.listening = false 175 | } 176 | 177 | // AcquireChangesChan create a new PDOMapChangeChan 178 | func (m *PDOMap) AcquireChangesChan() *PDOMapChangeChan { 179 | // Create frame chan 180 | chanID := uuid.Must(uuid.NewRandom()).String() 181 | changesChan := &PDOMapChangeChan{ 182 | ID: chanID, 183 | C: make(chan []byte), 184 | } 185 | 186 | // Append m.ChangeChans 187 | m.ChangeChans = append(m.ChangeChans, changesChan) 188 | 189 | return changesChan 190 | } 191 | 192 | // ReleaseChangesChan release (close) a PDOMapChangeChan 193 | func (m *PDOMap) ReleaseChangesChan(id string) error { 194 | var changesChan *PDOMapChangeChan 195 | var changesChanIndex *int 196 | 197 | for idx, fc := range m.ChangeChans { 198 | if fc.ID == id { 199 | changesChan = fc 200 | idxx := idx 201 | changesChanIndex = &idxx 202 | break 203 | } 204 | } 205 | 206 | if changesChanIndex == nil { 207 | return errors.New("no PDOMapChangeChan found with specified ID") 208 | } 209 | 210 | // Close chan 211 | close(changesChan.C) 212 | 213 | // Remove frameChan from network.FramesChans 214 | m.ChangeChans = append( 215 | m.ChangeChans[:*changesChanIndex], 216 | m.ChangeChans[*changesChanIndex+1:]..., 217 | ) 218 | 219 | return nil 220 | } 221 | 222 | // Read map values 223 | func (m *PDOMap) Read() error { 224 | // Get COB ID 225 | if err := m.ComRecord.FindIndex(1).Read(); err != nil { 226 | return err 227 | } 228 | 229 | cobID := int(*m.ComRecord.FindIndex(1).GetUintVal()) 230 | m.CobID = cobID 231 | 232 | // Is enabled 233 | m.Enabled = (int64(cobID) & MapPDONotValid) == 0 234 | 235 | // Is RTRAllowed 236 | m.RTRAllowed = (cobID & MapRTRNotAllowed) == 0 237 | 238 | // Get Trans type 239 | if err := m.ComRecord.FindIndex(2).Read(); err != nil { 240 | return err 241 | } 242 | 243 | transType := *m.ComRecord.FindIndex(2).GetUintVal() 244 | m.TransType = byte(transType) 245 | 246 | // Get EventTimer 247 | if transType > 254 { 248 | comr := m.ComRecord.FindIndex(5) 249 | 250 | if comr != nil { 251 | if err := comr.Read(); err != nil { 252 | return err 253 | } 254 | 255 | m.EventTimer = *comr.GetByteVal() 256 | } 257 | } 258 | 259 | // Init m.Map 260 | m.Map = make(map[int]DicObject) 261 | offset := 0 262 | 263 | // Nof entries 264 | if err := m.MapArray.FindIndex(0).Read(); err != nil { 265 | return err 266 | } 267 | 268 | nofEntries := int(m.MapArray.FindIndex(0).GetData()[0]) 269 | 270 | for i := 1; i <= (nofEntries + 1); i++ { 271 | ii := uint16(i) 272 | if err := m.MapArray.FindIndex(ii).Read(); err != nil { 273 | return err 274 | } 275 | 276 | val := *m.MapArray.FindIndex(ii).GetUintVal() 277 | 278 | index := uint16(val >> 16) 279 | subindex := uint16((val >> 8) & 0xFF) 280 | size := val & 0xFF 281 | 282 | if size == 0 { 283 | continue 284 | } 285 | 286 | dicVar := m.PDONode.Node.ObjectDic.FindIndex(index) 287 | // Instead of dicVar.Size = size @TODO: use uint64 for size 288 | dicVar.SetSize(int(size)) 289 | 290 | // Set sdo client 291 | dicVar.SetSDO(m.PDONode.Node.SDOClient) 292 | 293 | if !dicVar.IsDicVariable() { 294 | dicVar = dicVar.FindIndex(subindex) 295 | } 296 | 297 | dicVar.SetOffset(offset) 298 | // @TODO: check working 299 | m.Map[i] = dicVar 300 | 301 | // @TODO: use uint64 302 | offset += int(size) 303 | } 304 | 305 | m.UpdateDataSize() 306 | 307 | return m.Listen() 308 | } 309 | 310 | // Save pdo map 311 | // @TODO: Not Working, DO NOT USE 312 | func (m *PDOMap) Save() error { 313 | if true { 314 | return errors.New("PDOMap.Save() not implemented") 315 | } 316 | 317 | // Get COB ID 318 | if err := m.ComRecord.FindIndex(1).Read(); err != nil { 319 | return err 320 | } 321 | 322 | cobID := int(*m.ComRecord.FindIndex(1).GetUintVal()) 323 | m.CobID = cobID 324 | 325 | // Is enabled 326 | m.Enabled = (int64(cobID) & MapPDONotValid) == 0 327 | 328 | // Is RTRAllowed 329 | m.RTRAllowed = (cobID & MapRTRNotAllowed) == 0 330 | 331 | // Setting COB-ID 0x%X and temporarily disabling PDO 332 | m.ComRecord.FindIndex(1).SetData([]byte{byte(int64(cobID) | MapPDONotValid)}) 333 | if err := m.ComRecord.FindIndex(1).Save(); err != nil { 334 | return err 335 | } 336 | 337 | // Set transType 338 | if m.TransType != 0 { 339 | m.ComRecord.FindIndex(2).SetData([]byte{m.TransType}) 340 | if err := m.ComRecord.FindIndex(2).Save(); err != nil { 341 | return err 342 | } 343 | } 344 | 345 | if m.EventTimer != 0 { 346 | m.ComRecord.FindIndex(5).SetData([]byte{m.EventTimer}) 347 | if err := m.ComRecord.FindIndex(5).Save(); err != nil { 348 | return err 349 | } 350 | } 351 | 352 | if len(m.Map) > 0 { 353 | for i, dicVar := range m.Map { 354 | subIndex := i + 1 355 | val := ((((dicVar.GetIndex() << 16) | uint16(dicVar.GetSubIndex())) << 8) | uint16(dicVar.GetDataLen())) 356 | mm := m.MapArray.FindIndex(uint16(subIndex)) 357 | mm.SetUintVal(uint64(val)) 358 | 359 | if err := mm.Save(); err != nil { 360 | return err 361 | } 362 | } 363 | 364 | mapLen := len(m.Map) 365 | m.MapArray.FindIndex(0).SetIntVal(int64(mapLen)) 366 | if err := m.MapArray.FindIndex(0).Save(); err != nil { 367 | return err 368 | } 369 | 370 | m.UpdateDataSize() 371 | } 372 | 373 | return nil 374 | } 375 | 376 | // RebuildData rebuild map data object from map variables 377 | func (m *PDOMap) RebuildData() { 378 | data := make([]byte, m.GetTotalSize()/8) 379 | 380 | for _, dicVar := range m.Map { 381 | dicVarData := dicVar.GetData() 382 | if len(dicVarData) == 0 { 383 | continue 384 | } 385 | 386 | dicOffset := dicVar.GetOffset() / 8 387 | 388 | for i := 0; i < len(dicVarData); i++ { 389 | offset := i + dicOffset 390 | data[offset] = dicVarData[i] 391 | } 392 | } 393 | 394 | m.SetData(data) 395 | } 396 | 397 | // Transmit map data 398 | func (m *PDOMap) Transmit(rebuild bool) error { 399 | if rebuild { 400 | m.RebuildData() 401 | } 402 | 403 | return m.PDONode.Node.Network.Send(uint32(m.CobID), m.Data) 404 | } 405 | -------------------------------------------------------------------------------- /pdo_maps.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | // PDOMaps define a PDO map 4 | type PDOMaps struct { 5 | PDONode *PDONode 6 | Maps map[int]*PDOMap 7 | } 8 | 9 | // NewPDOMaps create a new Maps 10 | func NewPDOMaps(comOffset, mapOffset int, pdoNode *PDONode) *PDOMaps { 11 | pdoMaps := &PDOMaps{ 12 | PDONode: pdoNode, 13 | Maps: make(map[int]*PDOMap), 14 | } 15 | 16 | for i := 0; i < 32; i++ { 17 | if comSdo := pdoMaps.PDONode.Node.ObjectDic.FindIndex(uint16(comOffset + i)); comSdo != nil { 18 | mapSdo := pdoMaps.PDONode.Node.ObjectDic.FindIndex(uint16(mapOffset + i)) 19 | 20 | comSdo.SetSDO(pdoMaps.PDONode.Node.SDOClient) 21 | mapSdo.SetSDO(pdoMaps.PDONode.Node.SDOClient) 22 | 23 | pdoMaps.Maps[i+1] = NewPDOMap(pdoNode, comSdo, mapSdo) 24 | } 25 | } 26 | 27 | return pdoMaps 28 | } 29 | 30 | // FindIndex a map by index 31 | func (maps *PDOMaps) FindIndex(idx int) *PDOMap { 32 | if m, ok := maps.Maps[idx]; ok { 33 | return m 34 | } 35 | 36 | return nil 37 | } 38 | 39 | // FindName a map 40 | func (maps *PDOMaps) FindName(name string) *PDOMap { 41 | var m *PDOMap 42 | 43 | for _, ma := range maps.Maps { 44 | for _, v := range ma.Map { 45 | if v.GetName() == name { 46 | m = ma 47 | break 48 | } 49 | } 50 | 51 | if m != nil { 52 | break 53 | } 54 | } 55 | 56 | return m 57 | } 58 | -------------------------------------------------------------------------------- /pdo_node.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | type PDONode struct { 4 | Node *Node 5 | RX *PDOMaps 6 | TX *PDOMaps 7 | } 8 | 9 | func NewPDONode(n *Node) *PDONode { 10 | pdoNode := &PDONode{Node: n} 11 | pdoNode.RX = NewPDOMaps(0x1400, 0x1600, pdoNode) 12 | pdoNode.TX = NewPDOMaps(0x1800, 0x1A00, pdoNode) 13 | 14 | return pdoNode 15 | } 16 | 17 | func (node *PDONode) FindName(name string) *PDOMap { 18 | r := node.RX.FindName(name) 19 | if r == nil { 20 | r = node.TX.FindName(name) 21 | } 22 | 23 | return r 24 | } 25 | 26 | func (node *PDONode) Read() error { 27 | for _, maps := range []*PDOMaps{node.RX, node.TX} { 28 | for _, v := range maps.Maps { 29 | if err := v.Read(); err != nil { 30 | return err 31 | } 32 | } 33 | } 34 | 35 | return nil 36 | } 37 | 38 | func (node *PDONode) Save() error { 39 | for _, maps := range []*PDOMaps{node.RX, node.TX} { 40 | for _, v := range maps.Maps { 41 | if err := v.Save(); err != nil { 42 | return err 43 | } 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /sdo.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "github.com/angelodlfrtr/go-can" 8 | ) 9 | 10 | const ( 11 | SDORequestUpload uint8 = 2 << 5 12 | SDOResponseUpload uint8 = 2 << 5 13 | SDORequestDownload uint8 = 1 << 5 14 | SDOResponseDownload uint8 = 3 << 5 15 | 16 | SDORequestSegmentUpload uint8 = 3 << 5 17 | SDOResponseSegmentUpload uint8 = 0 << 5 18 | SDORequestSegmentDownload uint8 = 0 << 5 19 | SDOResponseSegmentDownload uint8 = 1 << 5 20 | 21 | SDOExpedited uint8 = 0x2 22 | SDOSizeSpecified uint8 = 0x1 23 | SDOToggleBit uint8 = 0x10 24 | SDONoMoreData uint8 = 0x1 25 | ) 26 | 27 | // SDOClient represent an SDO client 28 | type SDOClient struct { 29 | Node *Node 30 | RXCobID uint32 31 | TXCobID uint32 32 | SendQueue []string 33 | } 34 | 35 | func NewSDOClient(node *Node) *SDOClient { 36 | return &SDOClient{ 37 | Node: node, 38 | RXCobID: uint32(0x600 + node.ID), 39 | TXCobID: uint32(0x580 + node.ID), 40 | SendQueue: []string{}, 41 | } 42 | } 43 | 44 | // SendRequest to network bus 45 | func (sdoClient *SDOClient) SendRequest(req []byte) error { 46 | return sdoClient.Node.Network.Send(sdoClient.RXCobID, req) 47 | } 48 | 49 | // FindName find an sdo object from object dictionary by name 50 | func (sdoClient *SDOClient) FindName(name string) DicObject { 51 | if ob := sdoClient.Node.ObjectDic.FindName(name); ob != nil { 52 | ob.SetSDO(sdoClient) 53 | return ob 54 | } 55 | 56 | return nil 57 | } 58 | 59 | // Send message and optionaly wait for response 60 | func (sdoClient *SDOClient) Send( 61 | req []byte, 62 | expectFunc networkFramesChanFilterFunc, 63 | timeout *time.Duration, 64 | retryCount *int, 65 | ) (*can.Frame, error) { 66 | // If no response wanted, just send and return 67 | if expectFunc == nil { 68 | if err := sdoClient.SendRequest(req); err != nil { 69 | return nil, err 70 | } 71 | 72 | return nil, nil 73 | } 74 | 75 | // Set default timeout 76 | if timeout == nil { 77 | dtm := time.Duration(500) * time.Millisecond 78 | timeout = &dtm 79 | } 80 | 81 | if retryCount == nil { 82 | rtc := 4 83 | retryCount = &rtc 84 | } 85 | 86 | framesChan := sdoClient.Node.Network.AcquireFramesChan(expectFunc) 87 | 88 | // Retry loop 89 | remainingCount := *retryCount 90 | var frm *can.Frame 91 | 92 | for { 93 | if remainingCount == 0 { 94 | break 95 | } 96 | 97 | if err := sdoClient.SendRequest(req); err != nil { 98 | return nil, err 99 | } 100 | 101 | timer := time.NewTimer(*timeout) 102 | 103 | select { 104 | case <-timer.C: 105 | // Double timeout for each retry 106 | newTimeout := *timeout * 2 107 | timeout = &newTimeout 108 | case fr := <-framesChan.C: 109 | frm = fr 110 | } 111 | 112 | timer.Stop() 113 | remainingCount-- 114 | 115 | if frm != nil { 116 | break 117 | } 118 | } 119 | 120 | // Release data chan 121 | sdoClient.Node.Network.ReleaseFramesChan(framesChan.ID) 122 | 123 | // If no frm, timeout execeded 124 | if frm == nil { 125 | return nil, errors.New("timeout execeded") 126 | } 127 | 128 | return frm, nil 129 | } 130 | 131 | // Read sdo 132 | func (sdoClient *SDOClient) Read(index uint16, subIndex uint8) ([]byte, error) { 133 | reader := NewSDOReader(sdoClient, index, subIndex) 134 | return reader.ReadAll() 135 | } 136 | 137 | // Write sdo 138 | func (sdoClient *SDOClient) Write(index uint16, subIndex uint8, forceSegment bool, data []byte) error { 139 | writer := NewSDOWriter(sdoClient, index, subIndex, forceSegment) 140 | return writer.Write(data) 141 | } 142 | -------------------------------------------------------------------------------- /sdo_reader.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | 7 | "github.com/angelodlfrtr/go-can" 8 | ) 9 | 10 | type SDOReader struct { 11 | SDOClient *SDOClient 12 | Index uint16 13 | SubIndex uint8 14 | Toggle uint8 15 | Pos int 16 | Size uint32 17 | Data []byte 18 | } 19 | 20 | func NewSDOReader(sdoClient *SDOClient, index uint16, subIndex uint8) *SDOReader { 21 | return &SDOReader{ 22 | SDOClient: sdoClient, 23 | Index: index, 24 | SubIndex: subIndex, 25 | Data: []byte{}, 26 | } 27 | } 28 | 29 | // buildRequestUploadBuf working 30 | func (reader *SDOReader) buildRequestUploadBuf() []byte { 31 | buf := make([]byte, 8) // 8 len is important 32 | 33 | buf[0] = SDORequestUpload 34 | binary.LittleEndian.PutUint16(buf[1:], reader.Index) 35 | buf[3] = reader.SubIndex 36 | 37 | return buf 38 | } 39 | 40 | // buildRequestSegmentUploadBuf 41 | func (reader *SDOReader) buildRequestSegmentUploadBuf() []byte { 42 | buf := make([]byte, 8) 43 | 44 | command := SDORequestSegmentUpload 45 | command |= reader.Toggle 46 | buf[0] = command 47 | 48 | return buf 49 | } 50 | 51 | // RequestUpload returns data if EXPEDITED, else nil 52 | func (reader *SDOReader) RequestUpload() ([]byte, error) { 53 | expectFunc := func(frm *can.Frame) bool { 54 | resCommand := frm.Data[0] 55 | resIndex := binary.LittleEndian.Uint16(frm.Data[1:]) 56 | resSubindex := frm.Data[3] 57 | 58 | // Check response validity 59 | if (resCommand & 0xE0) != SDOResponseUpload { 60 | return false 61 | } 62 | 63 | if resIndex != reader.Index { 64 | return false 65 | } 66 | 67 | if resSubindex != reader.SubIndex { 68 | return false 69 | } 70 | 71 | return true 72 | } 73 | 74 | frm, err := reader.SDOClient.Send(reader.buildRequestUploadBuf(), &expectFunc, nil, nil) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | resCommand := frm.Data[0] 80 | resData := frm.Data[4:8] 81 | 82 | var expData []byte 83 | 84 | // If data is already in response (max 4 bytes) 85 | if (resCommand & SDOExpedited) != 0 { 86 | // Expedited upload 87 | if (resCommand & SDOSizeSpecified) != 0 { 88 | reader.Size = uint32(4 - ((resCommand >> 2) & 0x3)) 89 | expData = resData[0:reader.Size] 90 | } else { 91 | expData = resData 92 | } 93 | 94 | return expData, nil 95 | } 96 | 97 | if (resCommand & SDOSizeSpecified) != 0 { 98 | reader.Size = binary.LittleEndian.Uint32(resData[0:]) 99 | } 100 | 101 | // Will have to use segmented upload 102 | return nil, nil 103 | } 104 | 105 | // Read segmented uploads 106 | func (reader *SDOReader) Read() (*can.Frame, error) { 107 | expectFunc := func(frm *can.Frame) bool { 108 | if frm == nil { 109 | return false 110 | } 111 | 112 | if frm.ArbitrationID != reader.SDOClient.TXCobID { 113 | return false 114 | } 115 | 116 | resCommand := frm.Data[0] 117 | return (resCommand & 0xE0) == SDOResponseSegmentUpload 118 | } 119 | 120 | return reader.SDOClient.Send(reader.buildRequestSegmentUploadBuf(), &expectFunc, nil, nil) 121 | } 122 | 123 | // ReadAll .. 124 | func (reader *SDOReader) ReadAll() ([]byte, error) { 125 | data, err := reader.RequestUpload() 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | // If EXPEDITED, return data 131 | if data != nil { 132 | return data, nil 133 | } 134 | 135 | // Use Segmented upload 136 | for { 137 | frm, err := reader.Read() 138 | if err != nil { 139 | return nil, err 140 | } 141 | 142 | resCommand := frm.Data[0] 143 | if (resCommand & SDOToggleBit) != reader.Toggle { 144 | return nil, errors.New("toggle bit mismatch") 145 | } 146 | 147 | length := int(7 - ((resCommand >> 1) & 0x7)) 148 | reader.Toggle ^= SDOToggleBit 149 | reader.Pos += length 150 | 151 | // Append data 152 | reader.Data = append(reader.Data, frm.Data[1:length+1]...) 153 | 154 | // If no more data 155 | if (resCommand & SDONoMoreData) != 0 { 156 | break 157 | } 158 | 159 | // Continue, read next segment 160 | } 161 | 162 | return reader.Data, nil 163 | } 164 | -------------------------------------------------------------------------------- /sdo_writer.go: -------------------------------------------------------------------------------- 1 | package canopen 2 | 3 | import ( 4 | "encoding/binary" 5 | "errors" 6 | 7 | "github.com/angelodlfrtr/go-can" 8 | ) 9 | 10 | type SDOWriter struct { 11 | SDOClient *SDOClient 12 | Index uint16 13 | SubIndex uint8 14 | Done bool 15 | Toggle uint8 16 | Pos int 17 | Size uint32 18 | ForceSegment bool 19 | } 20 | 21 | func NewSDOWriter(sdoClient *SDOClient, index uint16, subIndex uint8, forceSegment bool) *SDOWriter { 22 | return &SDOWriter{ 23 | SDOClient: sdoClient, 24 | Index: index, 25 | SubIndex: subIndex, 26 | ForceSegment: forceSegment, 27 | } 28 | } 29 | 30 | // buildRequestDownloadBuf 31 | func (writer *SDOWriter) buildRequestDownloadBuf(data []byte, size *uint32) (string, []byte) { 32 | buf := make([]byte, 8) // 8 len is important 33 | command := SDORequestDownload 34 | 35 | if size != nil { 36 | command |= SDOSizeSpecified 37 | binary.LittleEndian.PutUint32(buf[4:], *size) 38 | } 39 | 40 | // Write object index / subindex 41 | binary.LittleEndian.PutUint16(buf[1:], writer.Index) 42 | buf[3] = writer.SubIndex 43 | 44 | // Segmented download 45 | if size == nil || ((size != nil) && *size > 4) || writer.ForceSegment { 46 | buf[0] = command 47 | return "segmented", buf 48 | } 49 | 50 | // Expedited download, so data is directly in download request message 51 | command = SDORequestDownload | SDOExpedited | SDOSizeSpecified 52 | command |= (4 - uint8(*size)) << 2 53 | buf[0] = command 54 | 55 | // Write data 56 | for i := 0; i < int(*size); i++ { 57 | buf[i+4] = data[i] 58 | } 59 | 60 | return "expedited", buf 61 | } 62 | 63 | // RequestDownload returns data if EXPEDITED, else nil 64 | func (writer *SDOWriter) RequestDownload(data []byte) error { 65 | // Get data size 66 | var size uint32 67 | 68 | if data != nil { 69 | size = uint32(len(data)) 70 | } 71 | 72 | downloadType, buf := writer.buildRequestDownloadBuf(data, &size) 73 | if downloadType == "segmented" { 74 | return errors.New("SDO segmented download not yet implemented") 75 | } 76 | 77 | expectFunc := func(frm *can.Frame) bool { 78 | resCommand := frm.Data[0] 79 | resIndex := binary.LittleEndian.Uint16(frm.Data[1:]) 80 | resSubindex := frm.Data[3] 81 | 82 | // Check response validity 83 | if (resCommand & 0xE0) != SDOResponseDownload { 84 | return false 85 | } 86 | 87 | if resIndex != writer.Index { 88 | return false 89 | } 90 | 91 | if resSubindex != writer.SubIndex { 92 | return false 93 | } 94 | 95 | return true 96 | } 97 | 98 | _, err := writer.SDOClient.Send(buf, &expectFunc, nil, nil) 99 | return err 100 | } 101 | 102 | // Write data to sdo client 103 | func (writer *SDOWriter) Write(data []byte) error { 104 | return writer.RequestDownload(data) 105 | } 106 | -------------------------------------------------------------------------------- /utils/contains.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | // ContainsUint32 return true if sub is present in arr 4 | func ContainsUint32(arr []uint32, sub uint32) bool { 5 | for _, a := range arr { 6 | if a == sub { 7 | return true 8 | } 9 | } 10 | 11 | return false 12 | } 13 | 14 | // ContainsByte return true if sub is present in arr 15 | func ContainsByte(arr []byte, sub byte) bool { 16 | for _, a := range arr { 17 | if a == sub { 18 | return true 19 | } 20 | } 21 | 22 | return false 23 | } 24 | -------------------------------------------------------------------------------- /utils/contains_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestContainsUint32(t *testing.T) { 8 | arr := []uint32{0x01, 0x02, 0x03} 9 | 10 | if !ContainsUint32(arr, uint32(0x03)) { 11 | t.Fatalf("ContainsUint32 with %v / %v should return true", arr, 0x03) 12 | } 13 | } 14 | 15 | func TestContainsByte(t *testing.T) { 16 | arr := []byte{0x01, 0x02, 0x03} 17 | 18 | if !ContainsByte(arr, byte(0x03)) { 19 | t.Fatalf("ContainsByte with %v / %v should return true", arr, 0x03) 20 | } 21 | } 22 | --------------------------------------------------------------------------------