├── VERSION ├── .gitignore ├── .dockerignore ├── version.go ├── internal └── driver │ ├── protocolpropertykey.go │ ├── config_test.go │ ├── config.go │ ├── readingchecker.go │ ├── incominglistener.go │ └── driver.go ├── cmd ├── main.go ├── res │ ├── docker │ │ └── configuration.toml │ ├── configuration.toml │ ├── VibrationDoc.yaml │ └── OpcuaServer.yaml └── Attribution.txt ├── bin └── edgex-launch.sh ├── Makefile ├── Dockerfile ├── Dockerfile_ARM64 ├── CHANGELOG.md ├── go.mod ├── README.md ├── README_CN.md └── LICENSE.txt /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.1 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.log 3 | cmd/device-opcua 4 | cmd/device-opcua-arm64 5 | go.sum 6 | vendor 7 | .idea 8 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.log 3 | cmd/device-opcua 4 | cmd/device-opcua-arm64 5 | go.sum 6 | vendor 7 | .idea 8 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | // -*- Mode: Go; indent-tabs-mode: t -*- 2 | // 3 | // Copyright (C) 2018 IOTech Ltd 4 | // 5 | // SPDX-License-Identifier: Apache-2.0 6 | 7 | package device_opcua 8 | 9 | // Global version for device-sdk-go 10 | var Version string = "to be replaced by makefile" 11 | -------------------------------------------------------------------------------- /internal/driver/protocolpropertykey.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | const ( 4 | Protocol = "opcua" 5 | 6 | Address = "Address" 7 | Port = "Port" 8 | Path = "Path" 9 | Policy = "Policy" 10 | Mode = "Mode" 11 | CertFile = "CertFile" 12 | KeyFile = "KeyFile" 13 | MappingStr = "Mapping" 14 | ) 15 | 16 | const SubscribeCommandName = "SubMark" -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/edgexfoundry/device-opcua-go" 5 | "github.com/edgexfoundry/device-opcua-go/internal/driver" 6 | "github.com/edgexfoundry/device-sdk-go/pkg/startup" 7 | ) 8 | 9 | const ( 10 | serviceName string = "edgex-device-opcua" 11 | ) 12 | 13 | func main() { 14 | sd := driver.NewProtocolDriver() 15 | startup.Bootstrap(serviceName, device_opcua.Version, sd) 16 | } 17 | -------------------------------------------------------------------------------- /bin/edgex-launch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Copyright (c) 2018 4 | # Mainflux 5 | # 6 | # SPDX-License-Identifier: Apache-2.0 7 | # 8 | 9 | ### 10 | # Launches all EdgeX Go binaries (must be previously built). 11 | # 12 | # Expects that Consul and MongoDB are already installed and running. 13 | # 14 | ### 15 | 16 | DIR=$PWD 17 | CMD=../cmd 18 | 19 | function cleanup { 20 | pkill edgex-device-opcua 21 | } 22 | 23 | cd $CMD 24 | exec -a edgex-device-opcua ./device-opcua & 25 | cd $DIR 26 | 27 | 28 | trap cleanup EXIT 29 | 30 | while : ; do sleep 1 ; done 31 | -------------------------------------------------------------------------------- /internal/driver/config_test.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/edgexfoundry/go-mod-core-contracts/models" 8 | ) 9 | 10 | func Test(t *testing.T) { 11 | 12 | protocols := map[string]models.ProtocolProperties{ 13 | Protocol: { 14 | Address: "192.168.3.165", 15 | Port: "53530", 16 | Path: "/OPCUA/SimulationServer", 17 | Policy: "None", 18 | Mode: "None", 19 | CertFile: "", 20 | KeyFile: "", 21 | MappingStr: "{ \"Counter\" = \"ns=5;s=Counter1\", \"Random\" = \"ns=5;s=Random1\" }", 22 | }, 23 | } 24 | 25 | q, _ := CreateConnectionInfo(protocols) 26 | fmt.Println(q) 27 | } 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test clean docker run build-arm64 docker-arm64 2 | 3 | GO=CGO_ENABLED=0 GO111MODULE=on go 4 | 5 | MICROSERVICES=cmd/device-opcua 6 | 7 | .PHONY: $(MICROSERVICES) $(MICROSERVICES-arm64) 8 | 9 | VERSION=$(shell cat ./VERSION) 10 | GIT_SHA=$(shell git rev-parse HEAD) 11 | 12 | GOFLAGS=-ldflags "-X github.com/edgexfoundry/device-opcua-go.Version=$(VERSION)" 13 | 14 | build: $(MICROSERVICES) 15 | $(GO) build ./... 16 | 17 | cmd/device-opcua: 18 | $(GO) build $(GOFLAGS) -o $@ ./cmd 19 | 20 | test: 21 | go test ./... -cover 22 | 23 | clean: 24 | rm -f $(MICROSERVICES) 25 | 26 | docker: 27 | docker build \ 28 | --label "git_sha=$(GIT_SHA)" \ 29 | -t burning1020/docker-device-opcua-go:$(VERSION)-dev \ 30 | . 31 | 32 | run: 33 | cd bin && ./edgex-launch.sh 34 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2018, 2019 Intel 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | FROM golang:1.11-alpine AS builder 7 | WORKDIR /go/src/github.com/edgexfoundry/device-opcua-go 8 | 9 | # Replicate the APK repository override. 10 | RUN sed -e 's/dl-cdn[.]alpinelinux.org/mirrors.ustc.edu.cn/g' -i~ /etc/apk/repositories 11 | 12 | # Install our build time packages. 13 | RUN apk update && apk add make git 14 | 15 | COPY . . 16 | 17 | RUN make build 18 | 19 | # Next image - Copy built Go binary into new workspace 20 | FROM scratch 21 | 22 | # expose command data port 23 | EXPOSE 49997 24 | 25 | COPY --from=builder /go/src/github.com/edgexfoundry/device-opcua-go/cmd / 26 | 27 | ENTRYPOINT ["/device-opcua","--profile=docker","--confdir=/res","--registry=consul://edgex-core-consul:8500"] 28 | -------------------------------------------------------------------------------- /Dockerfile_ARM64: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2018, 2019 Intel 3 | # 4 | # SPDX-License-Identifier: Apache-2.0 5 | # 6 | ARG ALPINE=golang:1.11-alpine 7 | FROM ${ALPINE} AS builder 8 | ARG ALPINE_PKG_BASE="build-base git openssh-client" 9 | ARG ALPINE_PKG_EXTRA="" 10 | 11 | # Replicate the APK repository override. 12 | # If it is no longer necessary to avoid the CDN mirros we should consider dropping this as it is brittle. 13 | RUN sed -e 's/dl-cdn[.]alpinelinux.org/mirrors.ustc.edu.cn/g' -i~ /etc/apk/repositories 14 | 15 | # Install our build time packages. 16 | RUN apk add ${ALPINE_PKG_BASE} ${ALPINE_PKG_EXTRA} 17 | 18 | WORKDIR $GOPATH/src/github.com/edgexfoundry/device-opcua-go 19 | 20 | COPY . . 21 | 22 | RUN make build-arm64 23 | 24 | # Next image - Copy built Go binary into new workspace 25 | FROM scratch 26 | 27 | ENV APP_PORT=49997 28 | #expose command data port 29 | EXPOSE $APP_PORT 30 | 31 | COPY --from=builder /go/src/github.com/edgexfoundry/device-opcua-go/cmd / 32 | 33 | CMD ["/device-opcua-arm64","--registry","--profile=docker","--confdir=/res"] 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [1.1.3] - 2020-03-05 11 | ### Fixed 12 | - cancel log out put when subscriptionData.json not exist or empty 13 | 14 | ## [1.1.2] - 2020-02-28 15 | ### Fixed 16 | - replace "address" with "host" in configuration 17 | - change one log level from ERROR to WARN 18 | 19 | ## [1.1.1] - 2020-01-07 20 | ### Added 21 | - CHANGELOG.md file to document any changes. 22 | - WaittingGroup for clean up jobs and Cancel function when stop driver. 23 | - save/load current devices subscription info in time. 24 | 25 | 26 | ### Fixed 27 | - Change "Origin" timestamp unit to micro seconds. 28 | - few times for execute defer functions. 29 | - replace time.After() with time.NewTicker() 30 | - cvs slice reallocate space 31 | 32 | ### Deprecated 33 | - Error Handler function when data dropped, low I/O effects of log file. 34 | - Logger Info for new data arrived, low I/O. -------------------------------------------------------------------------------- /cmd/res/docker/configuration.toml: -------------------------------------------------------------------------------- 1 | [Service] 2 | Host = "edgex-device-opcua" 3 | Port = 49997 4 | ConnectRetries = 3 5 | Labels = [] 6 | OpenMsg = "device simple started" 7 | ReadMaxLimit = 256 8 | Timeout = 5000 9 | EnableAsyncReadings = true 10 | AsyncBufferSize = 16 11 | 12 | [Registry] 13 | Host = "edgex-core-consul" 14 | Port = 8500 15 | CheckInterval = "10s" 16 | FailLimit = 3 17 | FailWaitTime = 10 18 | 19 | [Clients] 20 | [Clients.Data] 21 | Name = "edgex-core-data" 22 | Protocol = "http" 23 | Host = "edgex-core-data" 24 | Port = 48080 25 | Timeout = 5000 26 | 27 | [Clients.Metadata] 28 | Name = "edgex-core-metadata" 29 | Protocol = "http" 30 | Host = "edgex-core-metadata" 31 | Port = 48081 32 | Timeout = 5000 33 | 34 | [Clients.Logging] 35 | Name = "edgex-support-logging" 36 | Protocol = "http" 37 | Host = "edgex-support-logging" 38 | Port = 48061 39 | 40 | [Device] 41 | DataTransform = true 42 | InitCmd = "" 43 | InitCmdArgs = "" 44 | MaxCmdOps = 128 45 | MaxCmdValueLen = 256 46 | RemoveCmd = "" 47 | RemoveCmdArgs = "" 48 | ProfilesDir = "./res" 49 | 50 | [Logging] 51 | EnableRemote = true 52 | File = "/edgex/logs/device-opcua-go.log" 53 | Level = "INFO" 54 | 55 | [Writable] 56 | LogLevel = "INFO" 57 | 58 | # Pre-define Devices 59 | [[DeviceList]] 60 | Name = "SimulationServer" 61 | Profile = "OPCUA-Server" 62 | Description = "OPCUA device is created for test purpose" 63 | Labels = [ "test" ] 64 | [DeviceList.Protocols] 65 | [DeviceList.Protocols.opcua] 66 | Endpoint = "opc.tcp://Burning-Laptop:53530/OPCUA/SimulationServer" 67 | 68 | # Driver configs 69 | [Driver] 70 | #SubscribeJson = " {\"devices\":[{\"deviceName\":\"SimulationServer\",\"nodeIds\":[\"ns=5;s=Counter1\",\"ns=5;s=Random1\"],\"policy\":\"None\",\"mode\":\"None\",\"certFile\":\"\",\"keyFile\":\"\"}]} " -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/edgexfoundry/device-opcua-go 2 | 3 | require ( 4 | github.com/BurntSushi/toml v0.3.1 5 | github.com/edgexfoundry/device-sdk-go v1.1.1-0.20191104060856-f314af6bc7cd 6 | github.com/edgexfoundry/go-mod-core-contracts v0.1.31 7 | github.com/gopcua/opcua v0.0.0-20191111124717-762ddda1ae9a 8 | github.com/spf13/cast v1.3.0 9 | ) 10 | 11 | replace ( 12 | golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 => github.com/golang/crypto v0.0.0-20181029021203-45a5f77698d3 13 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 => github.com/golang/crypto v0.0.0-20190308221718-c2843e01d9a2 14 | golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 => github.com/golang/crypto v0.0.0-20190605123033-f99c8df09eb5 15 | golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4 => github.com/golang/crypto v0.0.0-20190621222207-cc06ce4a13d4 16 | ) 17 | 18 | replace ( 19 | golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 => github.com/golang/net v0.0.0-20181023162649-9b4f9f5ad519 20 | golang.org/x/net v0.0.0-20181201002055-351d144fa1fc => github.com/golang/net v0.0.0-20181201002055-351d144fa1fc 21 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 => github.com/golang/net v0.0.0-20190404232315-eb5bcb51f2a3 22 | ) 23 | 24 | replace golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 => github.com/golang/sync v0.0.0-20181221193216-37e7f081c4d4 25 | 26 | replace ( 27 | golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc => github.com/golang/sys v0.0.0-20180823144017-11551d06cbcc 28 | golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 => github.com/golang/sys v0.0.0-20181026203630-95b1ffbd15a5 29 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a => github.com/golang/sys v0.0.0-20190215142949-d0b11bdaac8a 30 | golang.org/x/sys v0.0.0-20190412213103-97732733099d => github.com/golang/sys v0.0.0-20190412213103-97732733099d 31 | golang.org/x/sys v0.0.0-20190602015325-4c4f7f33c9ed => github.com/golang/sys v0.0.0-20190602015325-4c4f7f33c9ed 32 | golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0 => github.com/golang/sys v0.0.0-20190624142023-c5567b49c5d0 33 | ) 34 | 35 | replace golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0 36 | -------------------------------------------------------------------------------- /cmd/res/configuration.toml: -------------------------------------------------------------------------------- 1 | [Service] 2 | Host = "localhost" 3 | Port = 49997 4 | ConnectRetries = 3 5 | Labels = [] 6 | OpenMsg = "device opcua golang started" 7 | Timeout = 5000 8 | EnableAsyncReadings = true 9 | AsyncBufferSize = 16 10 | 11 | [Registry] 12 | Host = "localhost" 13 | Port = 8500 14 | CheckInterval = "10s" 15 | FailLimit = 3 16 | FailWaitTime = 10 17 | Type = "consul" 18 | 19 | [Clients] 20 | [Clients.Data] 21 | Name = "edgex-core-data" 22 | Protocol = "http" 23 | Host = "localhost" 24 | Port = 48080 25 | Timeout = 5000 26 | 27 | [Clients.Metadata] 28 | Name = "edgex-core-metadata" 29 | Protocol = "http" 30 | Host = "localhost" 31 | Port = 48081 32 | Timeout = 5000 33 | 34 | [Clients.Logging] 35 | Name = "edgex-support-logging" 36 | Protocol = "http" 37 | Host = "localhost" 38 | Port = 48061 39 | 40 | [Device] 41 | DataTransform = true 42 | InitCmd = "" 43 | InitCmdArgs = "" 44 | MaxCmdOps = 12800 45 | MaxCmdValueLen = 256 46 | RemoveCmd = "" 47 | RemoveCmdArgs = "" 48 | ProfilesDir = "./res" 49 | 50 | [Logging] 51 | EnableRemote = false 52 | File = "./device-opcua.log" 53 | 54 | [Writable] 55 | LogLevel = "DEBUG" 56 | 57 | # Pre-define Devices 58 | #[[DeviceList]] 59 | # Name = "SimulationServer" 60 | # Profile = "OPCUA-Server" 61 | # Description = "OPCUA device is created for test purpose" 62 | # [DeviceList.Protocols] 63 | # [DeviceList.Protocols.opcua] 64 | ## Protocol = "opc.tcp" 65 | # Host = "192.168.3.165" 66 | # Port = "53530" 67 | # Path = "/OPCUA/SimulationServer" 68 | # MappingStr = "{ \"Counter\": \"ns=5;s=Counter1\", \"Random\": \"ns=5;s=Random1\" }" 69 | # Policy = "None" 70 | # Mode = "None" 71 | # CertFile = "" 72 | # KeyFile = "" 73 | # 74 | #[[DeviceList]] 75 | # Name = "Vibrator01" 76 | # Profile = "VibrationSensorDoc" 77 | # Description = "Vibrator for test at 192.168.3.188" 78 | # Labels = [ "VibrationSensor" ] 79 | # [DeviceList.Protocols] 80 | # [DeviceList.Protocols.opcua] 81 | # Host = "192.168.3.188" 82 | # Port = "4840" 83 | # MappingStr = "{ \"Vibration\": \"ns=1;i=115\", \"Switch\": \"ns=1;i=116\" }" -------------------------------------------------------------------------------- /internal/driver/config.go: -------------------------------------------------------------------------------- 1 | 2 | package driver 3 | 4 | import ( 5 | "fmt" 6 | "github.com/edgexfoundry/go-mod-core-contracts/models" 7 | "reflect" 8 | "strconv" 9 | ) 10 | 11 | const ( 12 | defaultProtocol = "opc.tcp" 13 | defaultPolicy = "None" 14 | defaultMode = "None" 15 | ) 16 | 17 | // Configuration can be configured in configuration.toml 18 | type Configuration struct { 19 | Protocol string `json:"protocol"` 20 | Host string `json:"host"` 21 | Port string `json:"port"` 22 | Path string `json:"path"` 23 | Policy string `json:"policy"` 24 | Mode string `json:"mode"` 25 | CertFile string `json:"cert_file"` 26 | KeyFile string `json:"key_file"` 27 | MappingStr string `json:"mapping_str"` 28 | } 29 | 30 | func (config *Configuration) setDefaultVal() { 31 | if config.Protocol == "" { 32 | config.Protocol = defaultProtocol 33 | } 34 | if config.Policy == "" { 35 | config.Policy = defaultPolicy 36 | } 37 | if config.Mode == "" { 38 | config.Mode = defaultMode 39 | } 40 | } 41 | // CreateConfigurationAndMapping use to load connectionInfo for read and write command 42 | func CreateConfigurationAndMapping(protocols map[string]models.ProtocolProperties) (*Configuration, map[string]string, error) { 43 | config := new(Configuration) 44 | protocol, ok := protocols[Protocol] 45 | if !ok { 46 | return nil, nil, fmt.Errorf("unable to load config, 'opcua' not exist") 47 | } 48 | err := load(protocol, config) 49 | if err != nil { 50 | return nil, nil, err 51 | } 52 | config.setDefaultVal() 53 | 54 | mapping, err := createNodeMapping(config.MappingStr) 55 | if err != nil { 56 | return config, nil, err 57 | } 58 | return config, mapping, nil 59 | } 60 | 61 | // load by reflect to check map key and then fetch the value 62 | func load(config map[string]string, des interface{}) error { 63 | val := reflect.ValueOf(des).Elem() 64 | for i := 0; i < val.NumField(); i++ { 65 | typeField := val.Type().Field(i) 66 | valueField := val.Field(i) 67 | 68 | val := config[typeField.Name] 69 | switch valueField.Kind() { 70 | case reflect.Int: 71 | intVal, err := strconv.Atoi(val) 72 | if err != nil { 73 | return err 74 | } 75 | valueField.SetInt(int64(intVal)) 76 | case reflect.String: 77 | valueField.SetString(val) 78 | default: 79 | return fmt.Errorf("none supported value type %v ,%v", valueField.Kind(), typeField.Name) 80 | } 81 | } 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /cmd/Attribution.txt: -------------------------------------------------------------------------------- 1 | The following open source projects are referenced by Device Service SDK Go: 2 | 3 | BurntSushi/toml (MIT) - https://github.com/BurntSushi/toml 4 | https://github.com/BurntSushi/toml/blob/master/COPYING 5 | 6 | gorilla/mux 1.6.2 (BSD-3) - https://github.com/gorilla/mux 7 | https://github.com/gorilla/mux/blob/master/LICENSE 8 | 9 | hashicorp/consul 1.1.0 (Mozilla Public License 2.0) - https://github.com/hashicorp/consul 10 | https://github.com/hashicorp/consul/blob/master/LICENSE 11 | 12 | hashicorp/go-cleanhttp (Mozilla Public License 2.0) - https://github.com/hashicorp/go-cleanhttp 13 | https://github.com/hashicorp/go-cleanhttp/blob/master/LICENSE 14 | 15 | hashicorp/go-rootcerts (Mozilla Public License 2.0) https://github.com/hashicorp/go-rootcerts 16 | https://github.com/hashicorp/go-rootcerts/blob/master/LICENSE 17 | 18 | mitchellh/go-homedir (MIT) https://github.com/mitchellh/go-homedir 19 | https://github.com/mitchellh/go-homedir/blob/master/LICENSE 20 | 21 | mitchellh/mapstructure (MIT) https://github.com/mitchellh/mapstructure 22 | https://github.com/mitchellh/mapstructure/blob/master/LICENSE 23 | 24 | hashicorp/serf (Mozilla Public License 2.0) https://github.com/hashicorp/serf 25 | https://github.com/hashicorp/serf/blob/master/LICENSE 26 | 27 | armon/go-metrics (MIT) https://github.com/armon/go-metrics 28 | https://github.com/armon/go-metrics/blob/master/LICENSE 29 | 30 | hashicorp/go-immutable-radix (Mozilla Public License 2.0) https://github.com/hashicorp/go-immutable-radix 31 | https://github.com/hashicorp/go-immutable-radix/blob/master/LICENSE 32 | 33 | hashicorp/golang-lru (Mozilla Public License 2.0) https://github.com/hashicorp/golang-lru 34 | https://github.com/hashicorp/golang-lru/blob/master/LICENSE 35 | 36 | robfig/cron (MIT) - https://github.com/robfig/cron 37 | https://github.com/robfig/cron/blob/master/LICENSE 38 | 39 | mgo v2 (BSD-2) - https://github.com/go-mgo/mgo 40 | https://github.com/go-mgo/mgo/blob/v2-unstable/LICENSE 41 | 42 | mgo/bson v2 (unspecified) - https://github.com/go-mgo/mgo/tree/v2/bson 43 | https://github.com/go-mgo/mgo/blob/v2/bson/LICENSE 44 | 45 | gopkg.in/yaml v2 (Apache 2.0) - https://github.com/go-yaml/yaml/tree/v2 46 | https://github.com/go-yaml/yaml/blob/v2/LICENSE 47 | 48 | edgexfoundry/edgex-go (Apache) - https://github.com/edgexfoundry/edgex-go 49 | https://github.com/edgexfoundry/edgex-go/blob/master/LICENSE 50 | 51 | gopcua/opcua (MIT) - https://github.com/gopcua/opcua 52 | https://github.com/gopcua/opcua/blob/master/LICENSE 53 | -------------------------------------------------------------------------------- /cmd/res/VibrationDoc.yaml: -------------------------------------------------------------------------------- 1 | name: "VibrationSensorDoc" 2 | model: "2" 3 | manufacturer: "Zhang Tianyang" 4 | labels: 5 | - "OPCUA" 6 | - "Siemens2019" 7 | description: "DeviceProfile for VibrationSensor in Wuhan Siemens" 8 | 9 | deviceResources: 10 | - name: "Vibration" 11 | description: "Vibration Sensor Date" 12 | properties: 13 | value: { type: "Int32", size: "4", readWrite: "R", defaultValue: "0", minimum: "0", maxmum: "1023" } 14 | units: { type: "String", readWrite: "R", defaultValue: "Bit" } 15 | 16 | - name: "Switch" 17 | description: "Switch Node to control alert" 18 | properties: 19 | value: { type: "Int32", size: "4", readWrite: "W", defaultValue: "0" } 20 | units: { type: "String", readWrite: "R", defaultValue: "1" } 21 | 22 | - name: "SubMark" 23 | description: "A Mark to distinguish Subscribe and common command" 24 | properties: 25 | value: { type: "string", readWrite: "W" } 26 | 27 | deviceCommands: 28 | - name: "Vibration" 29 | get: 30 | - { index: "1", operation: "get", deviceResource: "Vibration" } 31 | 32 | - name: "Alert" 33 | set: 34 | - { index: "1", operation: "set", deviceResource: "Switch" } 35 | 36 | - name: "Subscribe" 37 | set: 38 | - { index: "1", operation: "set", deviceResource: "SubMark" } 39 | - { index: "2", operation: "set", deviceResource: "Vibration", mappings: {"off": "0", "on": "1"} } 40 | 41 | coreCommands: 42 | - name: "Vibration" 43 | get: 44 | path: "/api/v1/device/{deviceId}/Vibration" 45 | responses: 46 | - code: "200" 47 | description: "OK" 48 | expectedValues: ["185", "186", "187"] 49 | - code: "503" 50 | description: "service unavailable" 51 | expectedValues: [] 52 | 53 | - name: "Alert" 54 | put: 55 | path: "/api/v1/device/{deviceId}/Alert" 56 | parameterNames: ["ns=1;i=116"] 57 | response: 58 | - code: "200" 59 | description: "OK" 60 | expectedValues: [] 61 | - code: "503" 62 | description: "service unavailable" 63 | expectedValues: [] 64 | 65 | - name: "Subscribe" 66 | put: 67 | path: "/api/v1/device/{deviceId}/Subscribe" 68 | parameterNames: ["Vibration"] 69 | response: 70 | - code: "200" 71 | description: "subscribe Vibration" 72 | expectedValues: [] 73 | - code: "503" 74 | description: "service unavailable" 75 | expectedValues: [] 76 | -------------------------------------------------------------------------------- /internal/driver/readingchecker.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "math" 5 | 6 | sdkModel "github.com/edgexfoundry/device-sdk-go/pkg/models" 7 | "github.com/spf13/cast" 8 | ) 9 | 10 | // checkValueInRange checks value range is valid 11 | func checkValueInRange(valueType sdkModel.ValueType, reading interface{}) bool { 12 | isValid := false 13 | 14 | if valueType == sdkModel.String || valueType == sdkModel.Bool { 15 | return true 16 | } 17 | 18 | if valueType == sdkModel.Int8 || valueType == sdkModel.Int16 || 19 | valueType == sdkModel.Int32 || valueType == sdkModel.Int64 { 20 | val := cast.ToInt64(reading) 21 | isValid = checkIntValueRange(valueType, val) 22 | } 23 | 24 | if valueType == sdkModel.Uint8 || valueType == sdkModel.Uint16 || 25 | valueType == sdkModel.Uint32 || valueType == sdkModel.Uint64 { 26 | val := cast.ToUint64(reading) 27 | isValid = checkUintValueRange(valueType, val) 28 | } 29 | 30 | if valueType == sdkModel.Float32 || valueType == sdkModel.Float64 { 31 | val := cast.ToFloat64(reading) 32 | isValid = checkFloatValueRange(valueType, val) 33 | } 34 | 35 | return isValid 36 | } 37 | 38 | func checkUintValueRange(valueType sdkModel.ValueType, val uint64) bool { 39 | var isValid = false 40 | switch valueType { 41 | case sdkModel.Uint8: 42 | if val >= 0 && val <= math.MaxUint8 { 43 | isValid = true 44 | } 45 | case sdkModel.Uint16: 46 | if val >= 0 && val <= math.MaxUint16 { 47 | isValid = true 48 | } 49 | case sdkModel.Uint32: 50 | if val >= 0 && val <= math.MaxUint32 { 51 | isValid = true 52 | } 53 | case sdkModel.Uint64: 54 | maxiMum := uint64(math.MaxUint64) 55 | if val >= 0 && val <= maxiMum { 56 | isValid = true 57 | } 58 | } 59 | return isValid 60 | } 61 | 62 | func checkIntValueRange(valueType sdkModel.ValueType, val int64) bool { 63 | var isValid = false 64 | switch valueType { 65 | case sdkModel.Int8: 66 | if val >= math.MinInt8 && val <= math.MaxInt8 { 67 | isValid = true 68 | } 69 | case sdkModel.Int16: 70 | if val >= math.MinInt16 && val <= math.MaxInt16 { 71 | isValid = true 72 | } 73 | case sdkModel.Int32: 74 | if val >= math.MinInt32 && val <= math.MaxInt32 { 75 | isValid = true 76 | } 77 | case sdkModel.Int64: 78 | if val >= math.MinInt64 && val <= math.MaxInt64 { 79 | isValid = true 80 | } 81 | } 82 | return isValid 83 | } 84 | 85 | func checkFloatValueRange(valueType sdkModel.ValueType, val float64) bool { 86 | var isValid = false 87 | switch valueType { 88 | case sdkModel.Float32: 89 | if math.Abs(val) >= math.SmallestNonzeroFloat32 && math.Abs(val) <= math.MaxFloat32 { 90 | isValid = true 91 | } 92 | case sdkModel.Float64: 93 | if math.Abs(val) >= math.SmallestNonzeroFloat64 && math.Abs(val) <= math.MaxFloat64 { 94 | isValid = true 95 | } 96 | } 97 | return isValid 98 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OPC-UA Device Service 2 | 3 | ## Overview 4 | This repository is a Go-based EdgeX Foundry Device Service which uses OPC-UA protocol to interact with the devices or IoT objects. 5 | 6 | Read [README_CN.md](./README_CN.md) for Chinese version. 7 | 8 | ## Features 9 | 1. Subscribe device node 10 | 2. Execute read command 11 | 2. Execute write command 12 | 13 | ## Prerequisite 14 | * MongoDB / Redis 15 | * Edgex-go: core data, core command, core metadata 16 | * OPCUA Server 17 | 18 | ## Predefined configuration 19 | 20 | ### Device Profile 21 | A Device Profile can be thought of as a template of a type or classification of Device. 22 | 23 | Write device profile for your own devices, define deviceResources, deviceCommands and coreCommands. Please refer to `cmd/res/OpcuaServer.yaml` 24 | 25 | Note: device profile must contains a "SubMark" string Value Descriptor and a "Subscribe" **SET** command if want to subscribe device node. 26 | And write **mappings** property. "SubMark" is used to distinguish Subscribe and other command. 27 | 28 | ### Pre-define Devices 29 | Define devices for device-sdk to auto upload device profile and create device instance. Please modify `configuration.toml` file which under `./cmd/res` folder. 30 | 31 | ```toml 32 | # Pre-define Devices 33 | [DeviceList.Protocols] 34 | [DeviceList.Protocols.opcua] 35 | Protocol = "opc.tcp" 36 | Address = "192.168.3.165" 37 | Port = "53530" 38 | Path = "/OPCUA/SimulationServer" 39 | MappingStr = "{ \"Counter\": \"ns=5;s=Counter1\", \"Random\": \"ns=5;s=Random1\" }" 40 | Policy = "None" 41 | Mode = "None" 42 | CertFile = "" 43 | KeyFile = "" 44 | ``` 45 | 46 | **Protocol**, **Policy**, **Mode**, **CertFile** and **KeyFile** properties are not necessary, they all have default value as mentioned above. 47 | 48 | Note: **MappingStr** property is JSON format and needs escape characters. 49 | 50 | ## Installation and Execution 51 | ```bash 52 | make build 53 | make run 54 | make docker 55 | ``` 56 | 57 | ## Subscribe device node 58 | Trigger a Subscribe command through these methods: 59 | 60 | - Edgex UI client. Sigh in -> Add Gateway and select it -> Select one DeviceService and click its Devices button -> 61 | Click target device's Commands button -> Select "Subscribe" set Method -> Ignore "SubMark" and fill the blank near ValueDescriptor, 62 | use "on" or "off" to represent subscribe this node or not. 63 | 64 | - Any HTTP Client like [PostMan](https://www.getpostman.com/). Use core command API to exec "subscribe" command. 65 | 66 | ## Reference 67 | * EdgeX Foundry Services: https://github.com/edgexfoundry/edgex-go 68 | * Go OPCUA library: https://github.com/gopcua/opcua 69 | * OPCUA Server: https://www.prosysopc.com/products/opc-ua-simulation-server 70 | 71 | ## Buy me a cup of coffee 72 | If you like this repository, star it and encourage me. -------------------------------------------------------------------------------- /cmd/res/OpcuaServer.yaml: -------------------------------------------------------------------------------- 1 | name: "OPCUA-Server" 2 | manufacturer: "Burning" 3 | labels: 4 | - "OPCUA" 5 | - "test" 6 | description: "Simulation results of OPCUA Server" 7 | 8 | deviceResources: 9 | - name: "Counter" 10 | description: "regular interval increment nums" 11 | properties: 12 | value: { type: "int32", size: "4", readWrite: "R", minimum: "0", maximum: "100" } 13 | units: { type: "String", readWrite: "R", defaultValue: "" } 14 | 15 | - name: "Random" 16 | description: "random decimal between 0 and 1" 17 | properties: 18 | value: { type: "float64", size: "8", floatEncoding: "eNotation", readWrite: "R", minimum: "0.00", maximum: "1.00" } 19 | units: { type: "String", readWrite: "R", defaultValue: "" } 20 | 21 | - name: "SubMark" 22 | description: "A Mark to distinguish Subscribe and common command" 23 | properties: 24 | value: { type: "string", readWrite: "W" } 25 | 26 | deviceCommands: 27 | - name: "Values" 28 | get: 29 | - { index: "1", operation: "get", deviceResource: "Counter" } 30 | - { index: "2", operation: "get", deviceResource: "Random" } 31 | set: 32 | - { index: "1", operation: "set", deviceResource: "Counter" } 33 | - { index: "2", operation: "set", deviceResource: "Random" } 34 | 35 | - name: "Counter" 36 | get: 37 | - { index: "1", operation: "get", deviceResource: "Counter" } 38 | 39 | - name: "Random" 40 | get: 41 | - { index: "1", operation: "get", deviceResource: "Random" } 42 | 43 | - name: "Subscribe" 44 | set: 45 | - { index: "1", operation: "set", deviceResource: "SubMark" } 46 | - { index: "2", operation: "set", deviceResource: "Counter", mappings: {"off": "0", "on": "1"} } 47 | - { index: "3", operation: "set", deviceResource: "Random" , mappings: {"off": "0", "on": "1"} } 48 | 49 | coreCommands: 50 | - name: "Values" 51 | get: 52 | path: "/api/v1/device/{deviceId}/Values" 53 | responses: 54 | - code: "200" 55 | description: "OK" 56 | expectedValues: ["Counter", "Random"] 57 | - code: "503" 58 | description: "service unavailable" 59 | expectedValues: [] 60 | put: 61 | path: "/api/v1/device/{deviceId}/Values" 62 | parameterNames: ["Counter", "Random"] 63 | response: 64 | - code: "200" 65 | description: "set Counter, Random" 66 | expectedValues: [] 67 | - code: "503" 68 | description: "service unavailable" 69 | expectedValues: [] 70 | 71 | - name: "Counter" 72 | get: 73 | path: "/api/v1/device/{deviceId}/Counter" 74 | responses: 75 | - code: "200" 76 | description: "" 77 | expectedValues: ["Counter"] 78 | - code: "503" 79 | description: "service unavailable" 80 | expectedValues: [] 81 | 82 | - name: "Random" 83 | get: 84 | path: "/api/v1/device/{deviceId}/Random" 85 | responses: 86 | - code: "200" 87 | description: "" 88 | expectedValues: ["Random"] 89 | - code: "503" 90 | description: "service unavailable" 91 | expectedValues: [] 92 | 93 | - name: "Subscribe" 94 | put: 95 | path: "/api/v1/device/{deviceId}/Subscribe" 96 | parameterNames: ["SubMark", "Counter", "Random"] 97 | response: 98 | - code: "200" 99 | description: "subscribe Counter, Random" 100 | expectedValues: [] 101 | - code: "503" 102 | description: "service unavailable" 103 | expectedValues: [] 104 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | #**树莓派上的Edgexfoundry实战** 2 | 3 | --- 4 | 5 | 此文档用于详细描述边缘计算框架degexfoundty部署到树莓派上的方法,可能出现的bug以及必要的说明 6 | 7 | **目录** 8 | 9 | [TOC] 10 | 11 | 12 | # 1 Edgexfoundry框架 13 | 14 | ## 1.1 简介 15 | + 官方文档 : 16 | + Core Service层:**core-data**, **core-metadata**, **core-command** 17 | + Support Service层 18 | + Export Service层 19 | + Device Service层:**device-mqtt** 20 | + EdgeX UI: 21 | + 官方github: 22 | + 官方维基: 23 | 24 | ## 1.2 device-opcua微服务 25 | 26 | device-opcua微服务位于Device Service层,与基于OPCUA协议的设备通讯 27 | 28 | ### 1.2.1 目标 29 | 30 | 针对基于**OPC-UA协议**的设备/传感器,基于官方github给出的 [device-skd-go](),定制**device-opcua微服务**的**golang**版本,调用opcua的 [go SDK]()接口,可以实现对此类设备的**注册**、**管理**、**控制**等,制作此微服务的**Docker镜像**,并部署在**树莓派3b+**,实现对OPCUA服务端节点的**读取、设置、监听**等操作。 31 | 32 | ### 1.2.2 准备 33 | 34 | 可提前准备好配置文件和Device Profile,方便设备微服务读取。服务启动后,配置信息可以通过[consul]()服务注入,Device Profile, Device的信息等可利用[Postman]()软件调用core-matadata服务的[API]()添加 35 | 36 | 1. `configuration.toml`文件提供device-opcua服务的信息、consul服务的信息、其他需要和设备服务交互的微服务的信息、Device信息(包含**Device Profile的目录**)、日志信息、预定义Schedule和SchedukeEvent信息(包含要**定时执行的命令**)、预定义设备信息(包含**设备的Endpoint信息**)、订阅设备及Node信息。 37 | 38 | 2. `configuration-driver.toml`文件提供OPCUA Server的NodeID与deviceResource的对应关系,以及监听操作的端点信息和设备资源对应关系 39 | 40 | 3. `OpcuaServer.yaml`作为设备的Device Profile, 有关它的书写参见[相关资料](#相关资料)1 2 41 | 42 | 43 | *注* :configuration.toml, configuration-driver.toml和Device Profile应确定好**唯一**的映射关系 44 | 45 | ### 1.2.4 已实现功能 46 | 47 | 1. OPCUA设备管理 48 | 2. 监听OPCUA节点上的值 49 | 3. 对指定节点进行读取操作 50 | 4. 对指定节点进行写入操作 51 | 5. 根据预定义的schedule执行命令 52 | 53 | Device Service的编写参考官方文档: 54 | 55 | 代码已提交至github仓库: 56 | 57 | ### 1.2.5 包管理 58 | 59 | 包管理工具可以自动下载代码所需要的依赖,不需要手工添加,只需要指定好包的地址和版本 60 | 61 | + edgex-go的deihi分支采用第三方包管理工具[glide](),利用glide.yaml下载代码所需依赖 62 | 63 | + go 1.11版本支持“modules”特性,使用[vgo]()作为包管理工具,利用go.mod文件下载配置依赖,master分支采用vgo 64 | 65 | *bug提示* :下载golang.org的包会被墙,要修改go.mod用github.com/golang替代源 66 | 67 | ### 1.2.6 Docker镜像制作 68 | 69 | 在`$GOPATH/src/github.com/edgexfoundry/device-opcua-go`目录下: 70 | 71 | + 执行`make build`命令编译可执行二进制文件device-opcua-go至cmd目录 72 | + 执行`make run`命令运行此可执行文件 73 | + 执行`make build-arm64`命令构建镜像,此镜像可放在树莓派内的Docker容器中运行,配置文件的读取参见[1.2.4](#1.2.4代码) 74 | 75 | ## 1.3 Export微服务 76 | 77 | Export 微服务可将数据导出到西门子工业云平台MindSphere 78 | 79 | 先将Open Edge Diver Kit置为export-distro的客户端,然后对其进行初始化并采集数据上云所需参数,最后发送数据 80 | 81 | 详见: Export-go: https://github.com/Burning1020/export-go 82 | 83 | # 2 OPC统一架构 84 | 85 | + OPC中国: 86 | 87 | + OPCUA虚拟设备: 88 | 89 | 90 | # 3 树莓派 91 | 92 | ## 3.1 准备 93 | 94 | 详见[相关资料](#相关资料)3 95 | 96 | + 烧录系统:推荐ubuntu 16.04 链接: https://pan.baidu.com/s/1gRXex4njLKi6dAHcqzrgjw 提取码: 9y5h 97 | 98 | + 安装Docker: 99 | 100 | + 安装Docker-Compose: 101 | 102 | *提示:*安装出现问题参见[相关资料](#相关资料)3 103 | 104 | ## 3.2 树莓派上部署egdexfoundry服务 105 | 106 | 修改docker-compose.yml文件,按需修改服务的image地址为`nexus.edgexfoundry.org:10004/<微服务>-go-arm64:`,具体详见: 107 | 108 | 拉取镜像的快慢取决于网络环境,网络环境差的可以参考[相关资料](#相关资料)3 109 | 110 | ## 3.3 树莓派上部署device-opcua服务 111 | 112 | ### 3.3.1 从docker hub上获取 113 | 114 | 将构建和的镜像打标签后推送至docker hub 115 | 116 | 117 | ```bash 118 | docker tag 119 | 120 | docker push 121 | ``` 122 | 123 | 124 | ### 3.3.2 从私有仓库中获取 125 | 126 | 详见:[同一局域网下搭建私有Docker仓库]() 127 | 128 | 同一局域网下,服务器和树莓派IP地址已知,让树莓派中运行的Docker拉取服务器中的镜像,在服务器中执行以下命令: 129 | 130 | 1. 创建本地仓库:`docker pull registry` 131 | 132 | 2. 容器中启动仓库: `docker run -d –p <端口:端口> <仓库名>` 133 | 134 | 3. 查看本地仓库:`curl 127.0.0.1:<端口>/v2/_catalog` 135 | 136 | 4. 打标签:`docker tag <目标镜像> <服务器IP:端口/镜像名>` 137 | 138 | 5. 修改push的HTTPS要求: `vim /etc/docker/daemon.json` 139 | 140 | { "insecure-registries": ["<服务器IP:端口>"] } 141 | 142 | 6. 重启docker:`systemctl restart docker` 143 | 144 | 7. 启动仓库,推送镜像:docker push 145 | 146 | ### 3.3.3 启动镜像 147 | 148 | 1. 向docker-compose.yml文件添加device-opcua服务,image地址为 149 | 150 | 2. 修改pull的HTTPS要求: `vim /etc/docker/daemon.json` 151 | 152 | { "insecure-registries": ["<服务器IP:端口>"] } 153 | 154 | 3. 重启docker:`systemctl restart docker` 155 | 156 | 4. 拉取并启动device-opcua服务:`docker-compose up -d device-opcua` 157 | 158 | # 4 云边协同 159 | 160 | ## 4.1 报警服务 161 | 162 | 待补充 163 | 164 | ## 4.2 Kubernetes 165 | 166 | 利用kubernetes将报警服务分发到节点并启动,当条件满足时触发报警动作 167 | 168 | # 相关资料 169 | 170 | 1. EdgeX Tech Talks (YouTube 频道需要翻墙) : 171 | 172 | 2. 边缘计算论坛: 173 | 174 | 3. EdgeX Foundry中国社区: 175 | 176 | 4. CSDN博客: 177 | 178 | + 179 | 180 | + 181 | 182 | 5. 阿里云云栖社区: 183 | 184 | # 捐赠 185 | 如果您喜欢这个项目,请在github上为我点赞,您的鼓励是我最大的动力! -------------------------------------------------------------------------------- /internal/driver/incominglistener.go: -------------------------------------------------------------------------------- 1 | // 2 | package driver 3 | 4 | import ( 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | sdk "github.com/edgexfoundry/device-sdk-go" 9 | sdkModel "github.com/edgexfoundry/device-sdk-go/pkg/models" 10 | "github.com/gopcua/opcua" 11 | "github.com/gopcua/opcua/monitor" 12 | "io/ioutil" 13 | "time" 14 | ) 15 | 16 | const ( 17 | DataPath = "subscriptionData.json" // the path of subscription data 18 | MassageChanCap = 16 // the capacity of massage chanel 19 | ReadingArrLen = 100 // the capacity of reading length 20 | WaitingDuration = 1000 * time.Millisecond // time duration of sent a event 21 | ) 22 | 23 | // CMS is a group of opcua_client, node monitor, subscription related, opcua_nodes and cancel func. 24 | type CMS struct { 25 | client *opcua.Client 26 | monitor *monitor.NodeMonitor 27 | sub *monitor.Subscription 28 | nodes map[string]bool // key-value struct of valueDescriptor name and subscribe state 29 | cancel context.CancelFunc // callback cancel function when stop subscription 30 | } 31 | 32 | // start listening for data change massage 33 | func startListening(deviceName string, config *Configuration, nodeMapping map[string]string, nodes map[string]bool) { 34 | subCtx, cancel := context.WithCancel(ctx) 35 | cms, exist := cmsMap[deviceName] 36 | if exist { 37 | var toAdd, toRemove []string // toAdd/toRemove represents new nodes to subscribe and old nodes to unsubscribe 38 | for node := range nodes{ 39 | if nodes[node] && !cms.nodes[node] { 40 | toAdd = append(toAdd, nodeMapping[node]) 41 | } else if !nodes[node] && cms.nodes[node] { 42 | toRemove = append(toRemove, nodeMapping[node]) 43 | } 44 | } 45 | _ = cms.sub.AddNodes(toAdd...) 46 | _ = cms.sub.RemoveNodes(toRemove...) 47 | cms.nodes = nodes // update cms when changed 48 | 49 | // we wish user always want to stop subscription, so let stop=true. 50 | // if any of node's state is true, which means user want to subscribe one node at least, so let stop=false. 51 | // if stop=true still, stop the subscription and delete CMS. 52 | stop := true 53 | for _, state := range cms.nodes { 54 | if state { 55 | stop = false 56 | break 57 | } 58 | } 59 | if stop { 60 | cms.cancel() 61 | delete(cmsMap, deviceName) 62 | } 63 | saveSubState() // save to file 64 | return 65 | } 66 | 67 | // nodeIds array contains the node ids that need to subscribe 68 | nodeIds := make([]string, 0) 69 | for node := range nodes { 70 | if nodes[node] { 71 | nodeIds = append(nodeIds, nodeMapping[node]) 72 | } 73 | } 74 | if len(nodeIds) < 1 { // all node state is off 75 | return 76 | } 77 | wg.Add(1) // wg is a WaitingGroup waiting for clean up work finished 78 | defer wg.Done() 79 | // create an opcua client and open connection based on config 80 | c, err := createClient(config) 81 | if err != nil { 82 | driver.Logger.Error(fmt.Sprintf("failed to create OPCUA c: %s", err)) 83 | return 84 | } 85 | defer c.Close() 86 | // create node Monitor 87 | nodeMonitor, _ := monitor.NewNodeMonitor(c) 88 | 89 | /** 90 | * @deprecated. handle function when data change massage was dropped 91 | * nodeMonitor.SetErrorHandler(func(_ *opcua.Client, sub *monitor.Subscription, err error) { 92 | * //driver.Logger.Error(fmt.Sprintf("error when subscribe device=%s : err=%s", deviceName, err.Error())) 93 | * }) 94 | */ 95 | // make a channel for data change 96 | notifyCh := make(chan *monitor.DataChangeMessage, MassageChanCap) 97 | defer close(notifyCh) 98 | 99 | sub, _ := nodeMonitor.ChanSubscribe(subCtx, notifyCh, nodeIds...) 100 | defer sub.Unsubscribe() 101 | cmsMap[deviceName] = &CMS{ 102 | client: c, 103 | monitor: nodeMonitor, 104 | sub: sub, 105 | nodes: nodes, 106 | cancel: cancel, 107 | } 108 | saveSubState() // save to file 109 | driver.Logger.Info(fmt.Sprintf("start subscribe device=%s", deviceName)) 110 | 111 | // reverse nodeMapping, bind nodeId with node 112 | for node, Id := range nodeMapping { 113 | nodeMapping[Id] = node 114 | } 115 | cvs := make([]*sdkModel.CommandValue, 0, ReadingArrLen) 116 | ticker := time.NewTicker(WaitingDuration) 117 | defer ticker.Stop() 118 | 119 | for { 120 | select { 121 | case <- subCtx.Done(): 122 | // cancel fun was called then ctx was done 123 | return 124 | case msg := <-notifyCh: 125 | deviceResource := nodeMapping[msg.NodeID.String()] 126 | cv := toCommandValue(msg.Value.Value(), deviceName, deviceResource) // reading 127 | cvs = append(cvs, cv) // event 128 | if len(cvs) >= ReadingArrLen { 129 | sentToAsynCh(cvs, deviceName) 130 | cvs = make([]*sdkModel.CommandValue, 0, ReadingArrLen) 131 | } 132 | case <- ticker.C: 133 | if len(cvs) > 0 { 134 | sentToAsynCh(cvs, deviceName) 135 | cvs = cvs[:0] 136 | //cvs = make([]*sdkModel.CommandValue, 0, ReadingArrLen) 137 | } 138 | } 139 | } 140 | } 141 | 142 | func toCommandValue(data interface{}, deviceName string, deviceResource string) *sdkModel.CommandValue { 143 | //driver.Logger.Info(fmt.Sprintf("[Incoming listener] Incoming reading received: name=%v deviceResource=%v value=%v", deviceName, deviceResource, data)) 144 | deviceObject, ok := sdk.RunningService().DeviceResource(deviceName, deviceResource, "get") 145 | if !ok { 146 | driver.Logger.Warn(fmt.Sprintf("[Incoming listener] Incoming reading ignored. No DeviceObject found: name=%v deviceResource=%v value=%v", deviceName, deviceResource, data)) 147 | return nil 148 | } 149 | 150 | req := sdkModel.CommandRequest{ 151 | DeviceResourceName: deviceResource, 152 | Type: sdkModel.ParseValueType(deviceObject.Properties.Value.Type), 153 | } 154 | 155 | result, err := newResult(req, data) 156 | if err != nil { 157 | driver.Logger.Warn(fmt.Sprintf("[Incoming listener] Incoming reading ignored. name=%v deviceResource=%v value=%v", deviceName, deviceResource, data)) 158 | return nil 159 | } 160 | return result 161 | } 162 | 163 | // sent event to asynchronous channel 164 | func sentToAsynCh(cvs []*sdkModel.CommandValue, deviceName string) { 165 | asyncValues := &sdkModel.AsyncValues{ 166 | DeviceName: deviceName, 167 | CommandValues: cvs, 168 | } 169 | driver.AsyncCh <- asyncValues 170 | } 171 | 172 | func saveSubState() { 173 | subState := make(map[string]map[string]bool) 174 | for deviceName, cms := range cmsMap { 175 | subState[deviceName] = cms.nodes 176 | } 177 | jsonStr, err := json.MarshalIndent(subState, "", " ") 178 | if err != nil { 179 | driver.Logger.Error(fmt.Sprintf("failed to marsh node state: %s", err)) 180 | return 181 | } 182 | if err = ioutil.WriteFile(DataPath, jsonStr, 0771); err != nil { 183 | driver.Logger.Error(fmt.Sprintf("failed to write %s: %s", DataPath, err)) 184 | } 185 | } 186 | 187 | func loadSubState() { 188 | subState := make(map[string]map[string]bool) 189 | b, err := ioutil.ReadFile(DataPath) 190 | if err != nil || b == nil { 191 | return 192 | } 193 | if err = json.Unmarshal(b, &subState); err != nil { 194 | driver.Logger.Error(fmt.Sprintf("failed to unmarshal data: %s", err)) 195 | } 196 | for deviceName, nodes := range subState { 197 | device, _ := sdk.RunningService().GetDeviceByName(deviceName) 198 | config, nodeMapping, _ := CreateConfigurationAndMapping(device.Protocols) 199 | go startListening(deviceName, config, nodeMapping, nodes) 200 | } 201 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2017 Dell, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /internal/driver/driver.go: -------------------------------------------------------------------------------- 1 | package driver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | sdkModel "github.com/edgexfoundry/device-sdk-go/pkg/models" 8 | "github.com/edgexfoundry/go-mod-core-contracts/clients/logger" 9 | "github.com/edgexfoundry/go-mod-core-contracts/models" 10 | "github.com/gopcua/opcua" 11 | "github.com/gopcua/opcua/ua" 12 | "github.com/spf13/cast" 13 | "sync" 14 | "time" 15 | ) 16 | 17 | var ( 18 | once sync.Once 19 | driver *Driver 20 | ctx context.Context 21 | cancel context.CancelFunc 22 | wg *sync.WaitGroup 23 | cmsMap map[string]*CMS 24 | ) 25 | 26 | type Driver struct { 27 | Logger logger.LoggingClient 28 | AsyncCh chan<- *sdkModel.AsyncValues 29 | } 30 | 31 | func NewProtocolDriver() sdkModel.ProtocolDriver { 32 | once.Do(func() { 33 | driver = new(Driver) 34 | }) 35 | return driver 36 | } 37 | 38 | // Initialize performs protocol-specific initialization for the device 39 | // service. 40 | func (d *Driver) Initialize(lc logger.LoggingClient, asyncCh chan<- *sdkModel.AsyncValues) error { 41 | ctx, cancel = context.WithCancel(context.Background()) 42 | d.Logger = lc 43 | d.AsyncCh = asyncCh 44 | wg = &sync.WaitGroup{} 45 | cmsMap = make(map[string]*CMS) 46 | loadSubState() 47 | return nil 48 | } 49 | 50 | func (d *Driver) DisconnectDevice(deviceName string, protocols map[string]models.ProtocolProperties) error { 51 | d.Logger.Warn("Driver was disconnected") 52 | return nil 53 | } 54 | 55 | // HandleReadCommands triggers a protocol Read operation for the specified device. 56 | func (d *Driver) HandleReadCommands(deviceName string, protocols map[string]models.ProtocolProperties, 57 | reqs []sdkModel.CommandRequest) ([]*sdkModel.CommandValue, error) { 58 | // load Protocol config 59 | config, nodeMapping, err := CreateConfigurationAndMapping(protocols) 60 | if err != nil { 61 | driver.Logger.Error(fmt.Sprintf("error create configuration: %s", err)) 62 | return nil, err 63 | } 64 | // create an opcua client and open connection based on config 65 | client, err := createClient(config) 66 | if err != nil { 67 | driver.Logger.Error(fmt.Sprintf("Failed to create OPCUA client: %s", err)) 68 | return nil, err 69 | } 70 | defer client.Close() 71 | 72 | responses := make([]*sdkModel.CommandValue, len(reqs)) 73 | for i, req := range reqs { 74 | nodeId, ok := nodeMapping[req.DeviceResourceName] 75 | if !ok { 76 | driver.Logger.Error(fmt.Sprintf("No NodeId found by DeviceResource:%s", req.DeviceResourceName)) 77 | continue 78 | } 79 | res, err := d.handleReadCommandRequest(client, req, nodeId) 80 | if err != nil { 81 | driver.Logger.Error(fmt.Sprintf("Handle read commands failed: %v", err)) 82 | continue 83 | } 84 | responses[i] = res 85 | } 86 | return responses, nil 87 | } 88 | 89 | func (d *Driver) handleReadCommandRequest(deviceClient *opcua.Client, req sdkModel.CommandRequest, nodeId string) (*sdkModel.CommandValue, error) { 90 | // get NewNodeID 91 | id, err := ua.ParseNodeID(nodeId) 92 | if err != nil { 93 | return nil, fmt.Errorf(fmt.Sprintf("Invalid node id=%s", nodeId)) 94 | } 95 | 96 | // make and execute ReadRequest 97 | request := &ua.ReadRequest{ 98 | MaxAge: 2000, 99 | NodesToRead: []*ua.ReadValueID{ 100 | &ua.ReadValueID{NodeID: id}, 101 | }, 102 | TimestampsToReturn: ua.TimestampsToReturnBoth, 103 | } 104 | resp, err := deviceClient.Read(request) 105 | if err != nil { 106 | return nil, fmt.Errorf(fmt.Sprintf("Read failed: %s", err)) 107 | } 108 | if resp.Results[0].Status != ua.StatusOK { 109 | return nil, fmt.Errorf(fmt.Sprintf("Status not OK: %v", resp.Results[0].Status)) 110 | } 111 | 112 | // make new result 113 | reading := resp.Results[0].Value.Value() 114 | result, err := newResult(req, reading) 115 | if err != nil { 116 | return nil, err 117 | } else { 118 | driver.Logger.Info(fmt.Sprintf("Get command finished: %v", result)) 119 | } 120 | return result, nil 121 | } 122 | 123 | // HandleWriteCommands passes a slice of CommandRequest struct each representing 124 | // a ResourceOperation for a specific device resource (aka DeviceObject). 125 | // Since the commands are actuation commands, params provide parameters for the individual 126 | // command. 127 | func (d *Driver) HandleWriteCommands(deviceName string, protocols map[string]models.ProtocolProperties, 128 | reqs []sdkModel.CommandRequest, params []*sdkModel.CommandValue) error { 129 | // load Protocol config 130 | config, nodeMapping, err := CreateConfigurationAndMapping(protocols) 131 | if err != nil { 132 | driver.Logger.Error(fmt.Sprintf("error create configuration: %s", err)) 133 | return err 134 | } 135 | 136 | if reqs[0].DeviceResourceName == SubscribeCommandName { 137 | // first parameter is $SubscribeCommandName means the command is to subscribe nodes 138 | nodes := make(map[string]bool) 139 | for i, req := range reqs[1 : ] { 140 | nodes[req.DeviceResourceName] = convert2TF(req.Type, params[i + 1]) 141 | } 142 | go startListening(deviceName, config, nodeMapping, nodes) 143 | return nil 144 | } 145 | // usual command 146 | // create an opcua client and open connection based on config 147 | client, err := createClient(config) 148 | if err != nil { 149 | driver.Logger.Error(fmt.Sprintf("Failed to create OPCUA client: %s", err)) 150 | return err 151 | } 152 | defer client.Close() 153 | 154 | for i, req := range reqs { 155 | nodeId, ok := nodeMapping[req.DeviceResourceName] 156 | if !ok { 157 | return fmt.Errorf(fmt.Sprintf("No NodeId found by DeviceResource:%s", req.DeviceResourceName)) 158 | } 159 | err := d.handleWriteCommandRequest(client, req, params[i], nodeId) 160 | if err != nil { 161 | return fmt.Errorf(fmt.Sprintf("Handle write commands failed: %v", err)) 162 | } 163 | } 164 | return nil 165 | } 166 | 167 | func (d *Driver) handleWriteCommandRequest(deviceClient *opcua.Client, req sdkModel.CommandRequest, 168 | param *sdkModel.CommandValue, nodeId string) error { 169 | // get NewNodeID 170 | id, err := ua.ParseNodeID(nodeId) 171 | if err != nil { 172 | return fmt.Errorf(fmt.Sprintf("Invalid node id=%s", nodeId)) 173 | } 174 | 175 | value, err := newCommandValue(req.Type, param) 176 | if err != nil { 177 | return err 178 | } 179 | v, err := ua.NewVariant(value) 180 | 181 | if err != nil { 182 | return fmt.Errorf(fmt.Sprintf("Invalid value: %v", err)) 183 | } 184 | 185 | request := &ua.WriteRequest{ 186 | NodesToWrite: []*ua.WriteValue{ 187 | &ua.WriteValue{ 188 | NodeID: id, 189 | AttributeID: ua.AttributeIDValue, 190 | Value: &ua.DataValue{ 191 | EncodingMask: uint8(13), // encoding mask 192 | Value: v, 193 | }, 194 | }, 195 | }, 196 | } 197 | 198 | resp, err := deviceClient.Write(request) 199 | if err != nil { 200 | driver.Logger.Error(fmt.Sprintf("Write value %v failed: %s", v, err)) 201 | return err 202 | } 203 | driver.Logger.Info(fmt.Sprintf("Write value %s %s", req.DeviceResourceName, resp.Results[0])) 204 | return nil 205 | } 206 | 207 | 208 | // Stop the protocol-specific DS code to shutdown gracefully, or 209 | // if the force parameter is 'true', immediately. The driver is responsible 210 | // for closing any in-use channels, including the channel used to send async 211 | // readings (if supported). 212 | func (d *Driver) Stop(force bool) error { 213 | d.Logger.Debug("Driver is doing clean up jobs...") 214 | cancel() 215 | wg.Wait() 216 | return nil 217 | } 218 | 219 | // AddDevice is a callback function that is invoked 220 | // when a new Device associated with this Device Service is added 221 | func (d *Driver) AddDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error { 222 | d.Logger.Debug(fmt.Sprintf("Device %s is updated", deviceName)) 223 | return nil 224 | } 225 | 226 | // UpdateDevice is a callback function that is invoked 227 | // when a Device associated with this Device Service is updated 228 | func (d *Driver) UpdateDevice(deviceName string, protocols map[string]models.ProtocolProperties, adminState models.AdminState) error { 229 | d.Logger.Debug(fmt.Sprintf("Device %s is updated", deviceName)) 230 | return nil 231 | } 232 | 233 | // RemoveDevice is a callback function that is invoked 234 | // when a Device associated with this Device Service is removed 235 | func (d *Driver) RemoveDevice(deviceName string, protocols map[string]models.ProtocolProperties) error { 236 | d.Logger.Debug(fmt.Sprintf("Device %s is updated", deviceName)) 237 | return nil 238 | } 239 | 240 | func createClient(config *Configuration) (*opcua.Client, error) { 241 | endpoint := fmt.Sprintf("%s://%s:%s%s", config.Protocol, config.Host, config.Port, config.Path) 242 | endpoints, err := opcua.GetEndpoints(endpoint) 243 | if err != nil { 244 | return nil, err 245 | } 246 | ep := opcua.SelectEndpoint(endpoints, config.Policy, ua.MessageSecurityModeFromString(config.Mode)) 247 | ep.EndpointURL = endpoint // replace 248 | if ep == nil { 249 | return nil, fmt.Errorf("failed to find suitable endpoint") 250 | } 251 | opts := []opcua.Option{ 252 | opcua.SecurityPolicy(config.Policy), 253 | opcua.SecurityModeString(config.Mode), 254 | opcua.CertificateFile(config.CertFile), 255 | opcua.PrivateKeyFile(config.KeyFile), 256 | opcua.SessionTimeout(30 * time.Minute), 257 | opcua.AuthAnonymous(), 258 | opcua.SecurityFromEndpoint(ep, ua.UserTokenTypeAnonymous), 259 | } 260 | client := opcua.NewClient(ep.EndpointURL, opts...) 261 | if err := client.Connect(ctx); err != nil { 262 | return nil, fmt.Errorf(fmt.Sprintf("Failed to create OPCUA client, %s", err)) 263 | } 264 | return client, nil 265 | } 266 | 267 | 268 | func createNodeMapping(mappingStr string) (map[string]string, error) { 269 | var mapping map[string]string 270 | b := []byte(mappingStr) 271 | if err := json.Unmarshal(b, &mapping); err != nil { 272 | return nil, fmt.Errorf(fmt.Sprintf("Umarshal failed: %s", err)) 273 | } 274 | return mapping, nil 275 | } 276 | 277 | func newResult(req sdkModel.CommandRequest, reading interface{}) (*sdkModel.CommandValue, error) { 278 | var result = &sdkModel.CommandValue{} 279 | var err error 280 | var resTime = time.Now().UnixNano() 281 | castError := "fail to parse %v reading, %v" 282 | 283 | if !checkValueInRange(req.Type, reading) { 284 | err = fmt.Errorf("parse reading fail. Reading %v is out of the value type(%v)'s range", reading, req.Type) 285 | driver.Logger.Error(err.Error()) 286 | return result, err 287 | } 288 | 289 | switch req.Type { 290 | case sdkModel.Bool: 291 | val, err := cast.ToBoolE(reading) 292 | if err != nil { 293 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 294 | } 295 | result, err = sdkModel.NewBoolValue(req.DeviceResourceName, resTime, val) 296 | case sdkModel.String: 297 | val, err := cast.ToStringE(reading) 298 | if err != nil { 299 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 300 | } 301 | result = sdkModel.NewStringValue(req.DeviceResourceName, resTime, val) 302 | case sdkModel.Uint8: 303 | val, err := cast.ToUint8E(reading) 304 | if err != nil { 305 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 306 | } 307 | result, err = sdkModel.NewUint8Value(req.DeviceResourceName, resTime, val) 308 | case sdkModel.Uint16: 309 | val, err := cast.ToUint16E(reading) 310 | if err != nil { 311 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 312 | } 313 | result, err = sdkModel.NewUint16Value(req.DeviceResourceName, resTime, val) 314 | case sdkModel.Uint32: 315 | val, err := cast.ToUint32E(reading) 316 | if err != nil { 317 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 318 | } 319 | result, err = sdkModel.NewUint32Value(req.DeviceResourceName, resTime, val) 320 | case sdkModel.Uint64: 321 | val, err := cast.ToUint64E(reading) 322 | if err != nil { 323 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 324 | } 325 | result, err = sdkModel.NewUint64Value(req.DeviceResourceName, resTime, val) 326 | case sdkModel.Int8: 327 | val, err := cast.ToInt8E(reading) 328 | if err != nil { 329 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 330 | } 331 | result, err = sdkModel.NewInt8Value(req.DeviceResourceName, resTime, val) 332 | case sdkModel.Int16: 333 | val, err := cast.ToInt16E(reading) 334 | if err != nil { 335 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 336 | } 337 | result, err = sdkModel.NewInt16Value(req.DeviceResourceName, resTime, val) 338 | case sdkModel.Int32: 339 | val, err := cast.ToInt32E(reading) 340 | if err != nil { 341 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 342 | } 343 | result, err = sdkModel.NewInt32Value(req.DeviceResourceName, resTime, val) 344 | case sdkModel.Int64: 345 | val, err := cast.ToInt64E(reading) 346 | if err != nil { 347 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 348 | } 349 | result, err = sdkModel.NewInt64Value(req.DeviceResourceName, resTime, val) 350 | case sdkModel.Float32: 351 | val, err := cast.ToFloat32E(reading) 352 | if err != nil { 353 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 354 | } 355 | result, err = sdkModel.NewFloat32Value(req.DeviceResourceName, resTime, val) 356 | case sdkModel.Float64: 357 | val, err := cast.ToFloat64E(reading) 358 | if err != nil { 359 | return nil, fmt.Errorf(castError, req.DeviceResourceName, err) 360 | } 361 | result, err = sdkModel.NewFloat64Value(req.DeviceResourceName, resTime, val) 362 | default: 363 | err = fmt.Errorf("return result fail, none supported value type: %v", req.Type) 364 | } 365 | 366 | return result, err 367 | } 368 | 369 | 370 | func convert2TF(valueType sdkModel.ValueType, param *sdkModel.CommandValue) bool { 371 | var TF bool 372 | value, err := newCommandValue(valueType, param) 373 | if err != nil { 374 | return false 375 | } 376 | switch valueType { 377 | case sdkModel.Bool: 378 | TF = value.(bool) 379 | case sdkModel.String: 380 | TF = value.(string) == "on" 381 | case sdkModel.Uint8: 382 | TF = value.(uint8) == uint8(1) 383 | case sdkModel.Uint16: 384 | TF = value.(uint16) == uint16(1) 385 | case sdkModel.Uint32: 386 | TF = value.(uint32) == uint32(1) 387 | case sdkModel.Uint64: 388 | TF = value.(uint64) == uint64(1) 389 | case sdkModel.Int8: 390 | TF = value.(int8) == int8(1) 391 | case sdkModel.Int16: 392 | TF = value.(int16) == int16(1) 393 | case sdkModel.Int32: 394 | TF = value.(int32) == int32(1) 395 | case sdkModel.Int64: 396 | TF = value.(int64) == int64(1) 397 | case sdkModel.Float32: 398 | TF = value.(float32) == float32(1) 399 | case sdkModel.Float64: 400 | TF = value.(float64) == float64(1) 401 | } 402 | return TF 403 | } 404 | 405 | func newCommandValue(valueType sdkModel.ValueType, param *sdkModel.CommandValue) (interface{}, error) { 406 | var commandValue interface{} 407 | var err error 408 | switch valueType { 409 | case sdkModel.Bool: 410 | commandValue, err = param.BoolValue() 411 | case sdkModel.String: 412 | commandValue, err = param.StringValue() 413 | case sdkModel.Uint8: 414 | commandValue, err = param.Uint8Value() 415 | case sdkModel.Uint16: 416 | commandValue, err = param.Uint16Value() 417 | case sdkModel.Uint32: 418 | commandValue, err = param.Uint32Value() 419 | case sdkModel.Uint64: 420 | commandValue, err = param.Uint64Value() 421 | case sdkModel.Int8: 422 | commandValue, err = param.Int8Value() 423 | case sdkModel.Int16: 424 | commandValue, err = param.Int16Value() 425 | case sdkModel.Int32: 426 | commandValue, err = param.Int32Value() 427 | case sdkModel.Int64: 428 | commandValue, err = param.Int64Value() 429 | case sdkModel.Float32: 430 | commandValue, err = param.Float32Value() 431 | case sdkModel.Float64: 432 | commandValue, err = param.Float64Value() 433 | default: 434 | err = fmt.Errorf("fail to convert param, none supported value type: %v", valueType) 435 | } 436 | 437 | return commandValue, err 438 | } --------------------------------------------------------------------------------