├── spa ├── vue.config.js ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── assets │ │ ├── error.png │ │ ├── logo.png │ │ ├── vuejs.svg │ │ └── go.svg │ ├── scss │ │ └── theme.scss │ ├── components │ │ ├── Error.vue │ │ ├── About.vue │ │ ├── Spinner.vue │ │ ├── Dial.vue │ │ ├── Monitor.vue │ │ ├── Info.vue │ │ └── Home.vue │ ├── mixins │ │ └── apiMixin.js │ ├── router.js │ ├── main.js │ ├── App.vue │ └── js │ │ └── gauge.min.js ├── README.md ├── .gitignore └── package.json ├── cmd ├── machinist │ ├── gen_device.bat │ ├── gen_device.sh │ ├── gen_certs.bat │ ├── gen_certs.sh │ ├── main.go │ └── routes.go └── streamer │ └── main.go ├── cloud ├── device.go └── config.go ├── device1_config.yaml ├── gateway_config.yaml ├── cloudbuild.tag.yaml ├── cloudbuild.yaml ├── opcua ├── publish.proto ├── write.proto ├── variant.proto ├── browse.proto ├── config.go ├── write.go ├── monitored-item.go ├── browse.go ├── variant.go ├── publish.pb.go ├── write.pb.go ├── device.go └── browse.pb.go ├── .gitignore ├── Dockerfile ├── design.md ├── LICENSE ├── gateway ├── config.go └── gateway.go ├── payload.md ├── go.mod ├── README.md └── device2_config.yaml /spa/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | productionSourceMap: false 3 | } -------------------------------------------------------------------------------- /spa/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awcullen/machinist/HEAD/spa/public/favicon.ico -------------------------------------------------------------------------------- /spa/src/assets/error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awcullen/machinist/HEAD/spa/src/assets/error.png -------------------------------------------------------------------------------- /spa/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awcullen/machinist/HEAD/spa/src/assets/logo.png -------------------------------------------------------------------------------- /cmd/machinist/gen_device.bat: -------------------------------------------------------------------------------- 1 | mkdir .\pki 2 | 3 | openssl ecparam -genkey -name prime256v1 -noout -out .\pki\device.key 4 | openssl req -x509 -new -days 3650 -subj "/CN=unused" -key .\pki\device.key -out .\pki\device.crt -------------------------------------------------------------------------------- /cmd/machinist/gen_device.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ./pki 4 | 5 | openssl ecparam -genkey -name prime256v1 -noout -out .\pki\device.key 6 | openssl req -x509 -new -days 3650 -subj "/CN=unused" -key .\pki\device.key -out .\pki\device.crt -------------------------------------------------------------------------------- /cloud/device.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | // Device is a communication channel to a local network endpoint. 4 | type Device interface { 5 | Name() string 6 | Location() string 7 | DeviceID() string 8 | Kind() string 9 | Stop() error 10 | } 11 | -------------------------------------------------------------------------------- /spa/src/scss/theme.scss: -------------------------------------------------------------------------------- 1 | /* 2 | $gr1: #52bf90; 3 | $gr2: #49ab81; 4 | $gr3: #419873; 5 | $gr4: #398564; 6 | $gr5: #317256; 7 | */ 8 | 9 | $theme-colors: ( 10 | "primary": #419873, 11 | "success": #49ab81, 12 | "info": #52bf90 13 | ); 14 | 15 | @import "node_modules/bootstrap/scss/bootstrap"; 16 | -------------------------------------------------------------------------------- /spa/README.md: -------------------------------------------------------------------------------- 1 | # Vue.js Single Page App 2 | 3 | ## Project setup 4 | ``` 5 | npm install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | npm run serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | npm run build 16 | 17 | in cmd/machinist/main.go -> go generate 18 | ``` 19 | -------------------------------------------------------------------------------- /spa/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | 23 | .npmrc -------------------------------------------------------------------------------- /spa/src/components/Error.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /spa/src/assets/vuejs.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /device1_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Line1 3 | description: Building1 4 | endpointURL: opc.tcp://127.0.0.1:55555 5 | securityPolicy: http://opcfoundation.org/UA/SecurityPolicy#None 6 | publishInterval: 5000.0 7 | samplingInterval: 1000.0 8 | queueSize: 16 9 | metrics: 10 | - metricID: 1-1 11 | nodeID: ns=2;s=Demo.Dynamic.Scalar.Float 12 | - metricID: 1-2 13 | nodeID: ns=2;s=Demo.Dynamic.Scalar.Double 14 | 15 | -------------------------------------------------------------------------------- /cmd/machinist/gen_certs.bat: -------------------------------------------------------------------------------- 1 | mkdir .\pki 2 | 3 | ( 4 | echo [req] 5 | echo distinguished_name=req 6 | echo [san] 7 | echo subjectAltName=URI:urn:127.0.0.1:device2,IP:127.0.0.1 8 | ) > .\pki\device2.cnf 9 | 10 | openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes ^ 11 | -keyout .\pki\device2.key -out .\pki\device2.crt ^ 12 | -subj /CN=device2 ^ 13 | -extensions san ^ 14 | -config .\pki\device2.cnf 15 | -------------------------------------------------------------------------------- /gateway_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: test-company 3 | location: New York, NY 4 | devices: 5 | - kind: opcua 6 | deviceID: device1 7 | privateKey: | 8 | -----BEGIN EC PRIVATE KEY----- 9 | -----END EC PRIVATE KEY----- 10 | algorithm: ES256 11 | - kind: opcua 12 | deviceID: device2 13 | privateKey: | 14 | -----BEGIN PRIVATE KEY----- 15 | -----END PRIVATE KEY----- 16 | algorithm: RS256 -------------------------------------------------------------------------------- /cloudbuild.tag.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: gcr.io/cloud-builders/docker 3 | entrypoint: /bin/bash 4 | args: 5 | - -c 6 | - > 7 | docker build 8 | --build-arg TAG_NAME=${TAG_NAME} 9 | --build-arg SHORT_SHA=${SHORT_SHA} 10 | -t gcr.io/${PROJECT_ID}/${_NAME}:${TAG_NAME} . 11 | 12 | images: 13 | - "gcr.io/${PROJECT_ID}/${_NAME}:${TAG_NAME}" 14 | 15 | substitutions: 16 | _NAME: machinist 17 | -------------------------------------------------------------------------------- /cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - name: gcr.io/cloud-builders/docker 3 | entrypoint: /bin/bash 4 | args: 5 | - -c 6 | - > 7 | docker build 8 | --build-arg TAG_NAME=${COMMIT_SHA} 9 | --build-arg SHORT_SHA=${SHORT_SHA} 10 | -t gcr.io/${PROJECT_ID}/${_NAME}:${COMMIT_SHA} . 11 | 12 | images: 13 | - "gcr.io/${PROJECT_ID}/${_NAME}:${COMMIT_SHA}" 14 | 15 | substitutions: 16 | _NAME: machinist 17 | -------------------------------------------------------------------------------- /opcua/publish.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package opcua; 3 | option go_package = ".;opcua"; 4 | import "opcua/variant.proto"; 5 | 6 | // MetricValue message 7 | message MetricValue { 8 | string name = 1; 9 | Variant value = 2; 10 | uint32 statusCode = 3; 11 | int64 timestamp = 4; 12 | } 13 | 14 | // PublishResponse message 15 | message PublishResponse { 16 | int64 timestamp = 1; 17 | repeated MetricValue metrics = 2; 18 | } 19 | -------------------------------------------------------------------------------- /cmd/machinist/gen_certs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | mkdir -p ./pki 4 | 5 | openssl req -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \ 6 | -keyout ./pki/server.key -out ./pki/server.crt \ 7 | -subj '/CN=test-server' \ 8 | -extensions san \ 9 | -config <(echo '[req]'; echo 'distinguished_name=req'; 10 | echo '[san]'; echo 'subjectAltName=URI:urn:127.0.0.1:test-server,IP:127.0.0.1') 11 | 12 | cat ./pki/server.crt >> ./pki/trusted.pem -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | 17 | # vscode settings 18 | .vscode/ 19 | 20 | .DS_Store 21 | *.env 22 | *.log 23 | *.swp 24 | bin/ 25 | *.csv 26 | debug 27 | **/bindata.go 28 | *~ 29 | *__debug_bin 30 | pki/ 31 | pkiusers/ 32 | data/ 33 | # secrets 34 | config.yaml -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION=1.14 2 | 3 | FROM golang:${GO_VERSION}-alpine AS build 4 | 5 | RUN apk add --no-cache git 6 | 7 | ARG TAG_NAME 8 | ARG SHORT_SHA 9 | 10 | WORKDIR /src 11 | COPY . . 12 | 13 | ENV CGO_ENABLED=0 GOOS=linux 14 | 15 | RUN go build -mod vendor -o /app \ 16 | -ldflags "-X main.version=$TAG_NAME -X main.commit=$SHORT_SHA" \ 17 | ./cmd/machinist 18 | 19 | FROM gcr.io/distroless/base AS run 20 | 21 | ADD https://pki.google.com/roots.pem /roots.pem 22 | ENV MACHINIST_CERTS /roots.pem 23 | 24 | COPY --from=build /app /machinist 25 | 26 | CMD ["/machinist"] 27 | -------------------------------------------------------------------------------- /design.md: -------------------------------------------------------------------------------- 1 | ## Implementation Details 2 | 3 | ### Google Cloud IoT Core 4 | 5 | Machinist communicates using MQTT to IoT Core through Gateways and Devices. 6 | 7 | Machinist subscribes to the following topics: 8 | - /devices/gateway-id/config 9 | - /devices/gateway-id/commands/# 10 | 11 | Machinist publishes to the following topics: 12 | - /devices/gateway-id/state 13 | - /devices/gateway-id/events/# 14 | 15 | When Machinist receives a config, it starts a machine for each device-id listed in config. 16 | 17 | Machines subscribe to the following topics: 18 | - /devices/device-id/config 19 | - /devices/device-id/commands/# 20 | 21 | Machine publish to the following topics: 22 | - /devices/device-id/state 23 | - /devices/device-id/events/# 24 | -------------------------------------------------------------------------------- /opcua/write.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package opcua; 3 | option go_package = ".;opcua"; 4 | import "opcua/variant.proto"; 5 | 6 | // The Write service definition. 7 | service WriteService { 8 | rpc Write(WriteRequest) returns (WriteResponse) {} 9 | } 10 | 11 | // WriteValue message. 12 | message WriteValue { 13 | string nodeId = 1; 14 | uint32 attributeId = 2; 15 | string indexRange = 3; 16 | Variant value = 4; 17 | } 18 | 19 | // WriteRequest message 20 | message WriteRequest { 21 | uint32 requestHandle = 1; 22 | repeated WriteValue nodesToWrite = 2; 23 | } 24 | 25 | // WriteResponse message. 26 | message WriteResponse { 27 | uint32 requestHandle = 1; 28 | uint32 statusCode = 2; 29 | repeated uint32 results = 3; 30 | } 31 | -------------------------------------------------------------------------------- /spa/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Machinist Admin 11 | 12 | 13 | 16 |
17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /spa/src/components/About.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /spa/src/mixins/apiMixin.js: -------------------------------------------------------------------------------- 1 | export default { 2 | data: function () { 3 | return { 4 | apiEndpoint: "/api" 5 | } 6 | }, 7 | 8 | methods: { 9 | apiGetHome: function() { 10 | return fetch(`${this.apiEndpoint}/home`) 11 | .then(resp => { 12 | return resp.json(); 13 | }) 14 | .catch(err => { 15 | console.log(`### API Error! ${err}`); 16 | }) 17 | }, 18 | 19 | apiGetMetrics: function() { 20 | return fetch(`${this.apiEndpoint}/metrics`) 21 | .then(resp => { 22 | return resp.json(); 23 | }) 24 | .catch(err => { 25 | console.log(`### API Error! ${err}`); 26 | }) 27 | }, 28 | 29 | apiGetInfo: function() { 30 | return fetch(`${this.apiEndpoint}/info`) 31 | .then(resp => { 32 | return resp.json(); 33 | }) 34 | .catch(err => { 35 | console.log(`### API Error! ${err}`); 36 | }) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /spa/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "machinist", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "@fortawesome/fontawesome-svg-core": "^1.2.26", 11 | "@fortawesome/free-brands-svg-icons": "^5.12.0", 12 | "@fortawesome/free-solid-svg-icons": "^5.12.0", 13 | "@fortawesome/vue-fontawesome": "^0.1.9", 14 | "bootstrap-vue": "^2.3.0", 15 | "vue": "^2.6.11", 16 | "vue-router": "^3.1.5", 17 | "vue-skycons": "^1.0.5" 18 | }, 19 | "devDependencies": { 20 | "@vue/cli-service": "^4.1.2", 21 | "node-sass": "^4.13.1", 22 | "sass-loader": "^8.0.2", 23 | "vue-template-compiler": "^2.6.11" 24 | }, 25 | "postcss": { 26 | "plugins": { 27 | "autoprefixer": {} 28 | } 29 | }, 30 | "browserslist": [ 31 | "Chrome >= 60", 32 | "Safari >= 10.1", 33 | "iOS >= 10.3", 34 | "Firefox >= 54", 35 | "Edge >= 15" 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /spa/src/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from './components/Home.vue' 4 | import About from './components/About.vue' 5 | import Error from './components/Error.vue' 6 | import Info from './components/Info.vue' 7 | import Monitor from './components/Monitor.vue' 8 | 9 | Vue.use(Router) 10 | 11 | export default new Router({ 12 | mode: 'history', 13 | routes: [ 14 | { 15 | path: '/', 16 | name: 'home', 17 | component: Home 18 | }, 19 | { 20 | path: '/home', 21 | name: 'apphome', 22 | component: Home 23 | }, 24 | { 25 | path: '/info', 26 | name: 'info', 27 | component: Info 28 | }, 29 | { 30 | path: '/monitor', 31 | name: 'monitor', 32 | component: Monitor 33 | }, 34 | { 35 | path: '/about', 36 | name: 'about', 37 | component: About 38 | }, 39 | { 40 | path: '*', 41 | name: 'catchall', 42 | component: Error 43 | } 44 | ] 45 | }) 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Andrew Cullen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /spa/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | 5 | // UI and Bootstrap stuff 6 | import BootstrapVue from 'bootstrap-vue' 7 | import 'bootstrap/dist/css/bootstrap.css' 8 | import 'bootstrap-vue/dist/bootstrap-vue.css' 9 | import './scss/theme.scss' 10 | 11 | // Font Awesome has Vue.js support, import some icons we'll use 12 | import { library as fontAwesomeLib } from '@fortawesome/fontawesome-svg-core' 13 | import { faHome, faCogs, faTachometerAlt, faInfoCircle, faUmbrella, faBomb } from '@fortawesome/free-solid-svg-icons' 14 | import { faGithub, faDocker } from '@fortawesome/free-brands-svg-icons' 15 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 16 | // Register icons and component 17 | fontAwesomeLib.add([faHome, faCogs, faTachometerAlt, faInfoCircle, faGithub, faDocker, faUmbrella, faBomb]) 18 | Vue.component('fa', FontAwesomeIcon) 19 | 20 | // We have to register this globally for some reason 21 | import VueSkycons from 'vue-skycons' 22 | Vue.use(VueSkycons) 23 | 24 | // Init Vue 25 | Vue.use(BootstrapVue); 26 | Vue.config.productionTip = false 27 | 28 | // Root Vue instance 29 | // Mount on the
and render the template of the App component 30 | new Vue({ 31 | router, 32 | render: h => h(App) 33 | }).$mount('#app') 34 | -------------------------------------------------------------------------------- /gateway/config.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "github.com/pkg/errors" 5 | "gopkg.in/yaml.v3" 6 | ) 7 | 8 | // Config ... 9 | type Config struct { 10 | Name string `yaml:"name"` 11 | Location string `yaml:"location,omitempty"` 12 | Devices []*DeviceConfig `yaml:"devices"` 13 | } 14 | 15 | // DeviceConfig ... 16 | type DeviceConfig struct { 17 | Kind string `yaml:"kind"` 18 | DeviceID string `yaml:"deviceID"` 19 | PrivateKey string `yaml:"privateKey"` 20 | Algorithm string `yaml:"algorithm"` 21 | } 22 | 23 | // unmarshalConfig unmarshals and validates the given payload. 24 | func unmarshalConfig(in []byte, out *Config) error { 25 | err := yaml.Unmarshal(in, out) 26 | if err != nil { 27 | return err 28 | } 29 | if out.Name == "" { 30 | return errors.New("missing value for field 'name'") 31 | } 32 | if len(out.Devices) == 0 { 33 | return errors.New("missing value for field 'devices'") 34 | } 35 | for _, dev := range out.Devices { 36 | if dev.DeviceID == "" { 37 | return errors.New("missing value for field 'deviceID'") 38 | } 39 | if !(dev.Kind == "opcua" || dev.Kind == "modbus") { 40 | return errors.New("value for field 'kind' must be one of 'opcua', 'modbus'") 41 | } 42 | //TODO: finish rest of validation 43 | } 44 | //TODO: validate there are no duplicate deviceIDs 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /spa/src/App.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 49 | -------------------------------------------------------------------------------- /spa/src/components/Spinner.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /payload.md: -------------------------------------------------------------------------------- 1 | Current metric payload: 2 | [ 3 | {"timestamp":1582898206000,"route":"/metric","uuid":"a2582b22-5e0e-11ea-bc5d-acde48001122","metric":"586b95c6","value":0.6046602879796196}, 4 | ... 5 | ] 6 | 7 | 10_000 metrics/gateway = metric message size: 1,392,753, compressed size: 160,408, ratio: 0.12 8 | 9 | 156 KB /sec 10 | 11 | 156 * 86400 KB /day 12 | 13 | Cloud IoT upload limit 512 KB/s 14 | 512 - 156 = 356 KB/s (potential headroom) 15 | 16 | it would take (156 * 86400) /356 /3600 = 10.5 hours to upload 24 hours of history 17 | 18 | 19 | New metric payload encoded in protobuf: 20 | { 21 | "timestamp":1582898206000, 22 | "metrics":[ 23 | {"name":"586b95c6","value":0.6046602879796196, "timestamp":1582898206000}, 24 | ... 25 | ] 26 | } 27 | 28 | 10,000 metric message size (encoded in protobuf, uncompressed): 300,700 29 | - or - 30 | 10,000 metric message size (encoded in protobuf, compressed): 165,500 31 | 162 KB /sec 32 | 33 | 162 * 86400 KB /day 34 | 35 | Cloud IoT upload limit 512 KB/s 36 | 512 - 162 = 350 KB/s (potential headroom) 37 | it would take (162 * 86400) /350 /3600 = 11.1 hours to upload 24 hours of history 38 | 39 | But, if instead of one device, there were 10 devices, each with 1000 metrics. 40 | 41 | 1,000 metric message size (encoded in protobuf, compressed): 16,550 42 | 16.2 KB /sec 43 | 44 | 16.2 * 86400 KB /day 45 | 46 | Cloud IoT upload limit 512 KB/s 47 | 512 - 16.2 = 495.8 KB/s (potential headroom) 48 | it would take (16.2 * 86400) /495.8 /3600 = 47 minutes to upload 24 hours of history 49 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/awcullen/machinist 2 | 3 | go 1.14 4 | 5 | require ( 6 | cloud.google.com/go v0.56.0 // indirect 7 | cloud.google.com/go/pubsub v1.3.1 8 | github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d // indirect 9 | github.com/awcullen/opcua v0.1.0-beta 10 | github.com/cenkalti/backoff/v4 v4.0.2 // indirect 11 | github.com/dgraph-io/badger/v2 v2.0.3 12 | github.com/dgraph-io/ristretto v0.0.2 // indirect 13 | github.com/dgrijalva/jwt-go v3.2.0+incompatible 14 | github.com/eclipse/paho.mqtt.golang v1.2.1-0.20200121105743-0d940dd29fd2 15 | github.com/gammazero/deque v0.0.0-20200310222745-50fa758af896 16 | github.com/go-ole/go-ole v1.2.4 // indirect 17 | github.com/golang/protobuf v1.4.0 18 | github.com/google/uuid v1.1.1 19 | github.com/gorilla/mux v1.7.4 20 | github.com/kr/pretty v0.2.0 // indirect 21 | github.com/pkg/errors v0.9.1 22 | github.com/rakyll/statik v0.1.7 23 | github.com/reactivex/rxgo/v2 v2.0.1 24 | github.com/shirou/gopsutil v2.20.4+incompatible 25 | github.com/spf13/cobra v1.0.0 26 | github.com/spf13/pflag v1.0.5 // indirect 27 | github.com/spf13/viper v1.6.3 28 | github.com/stretchr/objx v0.2.0 // indirect 29 | github.com/stretchr/testify v1.5.1 // indirect 30 | golang.org/x/sys v0.0.0-20200413165638-669c56c373c4 // indirect 31 | google.golang.org/api v0.21.0 // indirect 32 | google.golang.org/genproto v0.0.0-20200413115906-b5235f65be36 // indirect 33 | google.golang.org/grpc v1.29.1 34 | google.golang.org/protobuf v1.21.0 35 | gopkg.in/yaml.v2 v2.2.8 36 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c 37 | ) 38 | -------------------------------------------------------------------------------- /opcua/variant.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package opcua; 3 | option go_package = ".;opcua"; 4 | import "google/protobuf/timestamp.proto"; 5 | 6 | message Null {} 7 | message BooleanArray { repeated bool value = 1; } 8 | message Int32Array { repeated sint32 value = 1; } 9 | message UInt32Array { repeated uint32 value = 1; } 10 | message Int64Array { repeated sint64 value = 1; } 11 | message UInt64Array { repeated uint64 value = 1; } 12 | message FloatArray { repeated float value = 1; } 13 | message DoubleArray { repeated double value = 1; } 14 | message StringArray { repeated string value = 1; } 15 | message TimestampArray { repeated google.protobuf.Timestamp value = 1; } 16 | message BytesArray { repeated bytes value = 1; } 17 | 18 | // Variant message. 19 | message Variant { 20 | oneof value { 21 | Null Null = 127; 22 | bool Boolean = 1; 23 | sint32 SByte = 2; 24 | uint32 Byte = 3; 25 | sint32 Int16 = 4; 26 | uint32 UInt16 = 5; 27 | sint32 Int32 = 6; 28 | uint32 UInt32 = 7; 29 | sint64 Int64 = 8; 30 | uint64 UInt64 = 9; 31 | float Float = 10; 32 | double Double = 11; 33 | string String = 12; 34 | google.protobuf.Timestamp DateTime = 13; 35 | bytes Guid = 14; 36 | bytes ByteString = 15; 37 | BooleanArray BooleanArray = 51; 38 | Int32Array SByteArray = 52; 39 | UInt32Array ByteArray = 53; 40 | Int32Array Int16Array = 54; 41 | UInt32Array UInt16Array = 55; 42 | Int32Array Int32Array = 56; 43 | UInt32Array UInt32Array = 57; 44 | Int64Array Int64Array = 58; 45 | UInt64Array UInt64Array = 59; 46 | FloatArray FloatArray = 60; 47 | DoubleArray DoubleArray = 61; 48 | StringArray StringArray = 62; 49 | TimestampArray DateTimeArray = 63; 50 | BytesArray GuidArray = 64; 51 | BytesArray ByteStringArray = 65; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /opcua/browse.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package opcua; 3 | option go_package = ".;opcua"; 4 | 5 | // The Browse service definition. 6 | service BrowseService { 7 | rpc Browse(BrowseRequest) returns (BrowseResponse) {} 8 | rpc BrowseNext(BrowseNextRequest) returns (BrowseNextResponse) {} 9 | } 10 | 11 | enum BrowseDirectionEnum { 12 | FORWARD = 0; 13 | INVERSE = 1; 14 | BOTH = 2; 15 | INVALID = 3; 16 | } 17 | 18 | enum NodeClass { 19 | UNSPECIFIED = 0; 20 | OBJECT = 1; 21 | VARIABLE = 2; 22 | METHOD = 4; 23 | OBJECT_TYPE = 8; 24 | VARIABLE_TYPE = 16; 25 | REFERENCE_TYPE = 32; 26 | DATA_TYPE = 64; 27 | VIEW = 128; 28 | } 29 | 30 | enum BrowseResultMask { 31 | NONE = 0; 32 | REFERENCE_TYPE_ID = 1; 33 | IS_FORWARD = 2; 34 | NODE_CLASS = 4; 35 | BROWSE_NAME = 8; 36 | DISPLAY_NAME = 16; 37 | TYPE_DEFINITION = 32; 38 | ALL = 63; 39 | REFERENCE_TYPE_INFO = 3; 40 | TARGET_INFO = 60; 41 | } 42 | 43 | // BrowseDescription message 44 | message BrowseDescription { 45 | string nodeId = 1; 46 | BrowseDirectionEnum browseDirection = 2; 47 | string referenceTypeId = 3; 48 | bool includeSubtypes = 4; 49 | uint32 nodeClassMask = 5; 50 | uint32 resultMask = 6; 51 | } 52 | 53 | // BrowseRequest message 54 | message BrowseRequest { 55 | uint32 requestHandle = 1; 56 | uint32 requestedMaxReferencesPerNode = 2; 57 | repeated BrowseDescription nodesToBrowse = 3; 58 | } 59 | 60 | // ReferenceDescription message. 61 | message ReferenceDescription { 62 | string referenceTypeId = 1; 63 | bool isForward = 2; 64 | string nodeId = 3; 65 | string browseName = 4; 66 | string displayName = 5; 67 | uint32 nodeClass = 6; 68 | string typeDefinition = 7; 69 | } 70 | 71 | // BrowseResult message. 72 | message BrowseResult { 73 | uint32 statusCode = 1; 74 | bytes continuationPoint = 2; 75 | repeated ReferenceDescription references = 3; 76 | } 77 | 78 | // BrowseResponse message. 79 | message BrowseResponse { 80 | uint32 requestHandle = 1; 81 | uint32 statusCode = 2; 82 | repeated BrowseResult results = 3; 83 | } 84 | 85 | message BrowseNextRequest { 86 | uint32 requestHandle = 1; 87 | bool releaseContinuationPoints = 2; 88 | repeated bytes continuationPoints = 3; 89 | } 90 | 91 | message BrowseNextResponse { 92 | uint32 requestHandle = 1; 93 | uint32 statusCode = 2; 94 | repeated BrowseResult results = 3; 95 | } -------------------------------------------------------------------------------- /spa/src/components/Dial.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 86 | 87 | 101 | -------------------------------------------------------------------------------- /spa/src/components/Monitor.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 83 | 84 | -------------------------------------------------------------------------------- /cloud/config.go: -------------------------------------------------------------------------------- 1 | package cloud 2 | 3 | import ( 4 | "crypto/x509" 5 | "io/ioutil" 6 | "log" 7 | "net/http" 8 | "sync" 9 | "time" 10 | 11 | "github.com/dgrijalva/jwt-go" 12 | "github.com/pkg/errors" 13 | ) 14 | 15 | const ( 16 | MqttBrokerURL = "tls://mqtt.googleapis.com:443" 17 | CACertsURL = "https://pki.goog/roots.pem" 18 | ProtocolVersion = 4 // corresponds to MQTT 3.1.1 19 | ReconnectDelay = 20 * time.Second 20 | TokenDuration = 24 * time.Hour // maximum 24 h 21 | ) 22 | 23 | var ( 24 | once sync.Once 25 | caCertPool *x509.CertPool 26 | ) 27 | 28 | // Config stores the data for connecting to the MQTT broker. 29 | type Config struct { 30 | ProjectID string `yaml:"projectID"` 31 | Region string `yaml:"region"` 32 | RegistryID string `yaml:"registryID"` 33 | DeviceID string `yaml:"deviceID"` 34 | PrivateKey string `yaml:"privateKey"` 35 | Algorithm string `yaml:"algorithm"` 36 | StorePath string `yaml:"storePath"` 37 | } 38 | 39 | // CreateJWT creates a Cloud IoT Core JWT for the given project id. 40 | func CreateJWT(projectID, privateKey, algorithm string, iat, exp time.Time) (string, error) { 41 | 42 | claims := jwt.StandardClaims{ 43 | Audience: projectID, 44 | IssuedAt: iat.Unix(), 45 | ExpiresAt: exp.Unix(), 46 | } 47 | 48 | token := jwt.NewWithClaims(jwt.GetSigningMethod(algorithm), claims) 49 | 50 | switch algorithm { 51 | case "RS256": 52 | privKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(privateKey)) 53 | if err != nil { 54 | return "", err 55 | } 56 | return token.SignedString(privKey) 57 | case "ES256": 58 | privKey, err := jwt.ParseECPrivateKeyFromPEM([]byte(privateKey)) 59 | if err != nil { 60 | return "", err 61 | } 62 | return token.SignedString(privKey) 63 | } 64 | return "", errors.New("invalid token algorithm, need one of ES256 or RS256") 65 | } 66 | 67 | // GetCACertPool gets the latest Google CA certs from 'https://pki.goog/roots.pem' 68 | func GetCACertPool() *x509.CertPool { 69 | once.Do(func() { 70 | // Load Google Root CA certs. 71 | resp, err := http.Get(CACertsURL) 72 | if err != nil { 73 | log.Println(errors.Wrapf(err, "cannot get cert file from '%s'", CACertsURL)) 74 | return 75 | } 76 | defer resp.Body.Close() 77 | certs, err := ioutil.ReadAll(resp.Body) 78 | if err != nil { 79 | log.Println(errors.Wrapf(err, "cannot read cert file from '%s'", CACertsURL)) 80 | return 81 | } 82 | roots := x509.NewCertPool() 83 | if ok := roots.AppendCertsFromPEM(certs); !ok { 84 | log.Println(errors.Errorf("cannot parse cert file from '%s'", CACertsURL)) 85 | return 86 | } 87 | caCertPool = roots 88 | }) 89 | 90 | return caCertPool 91 | } 92 | -------------------------------------------------------------------------------- /opcua/config.go: -------------------------------------------------------------------------------- 1 | package opcua 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "gopkg.in/yaml.v2" 8 | ) 9 | 10 | // Config ... 11 | type Config struct { 12 | Name string `yaml:"name"` 13 | Location string `yaml:"location,omitempty"` 14 | EndpointURL string `yaml:"endpointURL"` 15 | SecurityPolicy string `yaml:"securityPolicy,omitempty"` 16 | Username string `yaml:"username,omitempty"` 17 | Password string `yaml:"password,omitempty"` 18 | PublishInterval float64 `yaml:"publishInterval"` 19 | SamplingInterval float64 `yaml:"samplingInterval"` 20 | QueueSize uint32 `yaml:"queueSize"` 21 | PublishTTL time.Duration `yaml:"publishTTL"` 22 | Metrics []*Metric `yaml:"metrics"` 23 | } 24 | 25 | // Metric ... 26 | type Metric struct { 27 | MetricID string `yaml:"metricID"` 28 | NodeID string `yaml:"nodeID"` 29 | AttributeID uint32 `yaml:"attributeID,omitempty"` 30 | IndexRange string `yaml:"indexRange,omitempty"` 31 | SamplingInterval float64 `yaml:"samplingInterval,omitempty"` 32 | QueueSize uint32 `yaml:"queueSize,omitempty"` 33 | } 34 | 35 | // unmarshalConfig unmarshals and validates the given payload. 36 | func unmarshalConfig(in []byte, out *Config) error { 37 | err := yaml.Unmarshal(in, out) 38 | if err != nil { 39 | return err 40 | } 41 | if out.Name == "" { 42 | return errors.New("missing value for field 'name'") 43 | } 44 | if out.EndpointURL == "" { 45 | return errors.New("missing value for field 'endpointURL'") 46 | } 47 | if out.PublishInterval == 0 { 48 | return errors.New("missing value for field 'publishInterval'") 49 | } 50 | if out.SamplingInterval == 0 { 51 | return errors.New("missing value for field 'samplingInterval'") 52 | } 53 | if out.QueueSize == 0 { 54 | return errors.New("missing value for field 'queueSize'") 55 | } 56 | if out.PublishTTL == 0 { 57 | out.PublishTTL = 24 * time.Hour 58 | } 59 | if len(out.Metrics) == 0 { 60 | return errors.New("missing value for field 'metrics'") 61 | } 62 | 63 | for _, metric := range out.Metrics { 64 | if metric.MetricID == "" { 65 | return errors.New("missing value for field 'metricID'") 66 | } 67 | if metric.NodeID == "" { 68 | return errors.New("missing value for field 'nodeID'") 69 | } 70 | if metric.AttributeID == 0 { 71 | metric.AttributeID = 13 72 | } 73 | if metric.SamplingInterval == 0 { 74 | metric.SamplingInterval = out.SamplingInterval 75 | } 76 | if metric.QueueSize == 0 { 77 | metric.QueueSize = out.QueueSize 78 | } 79 | } 80 | //TODO: validate there are no duplicate metricIDs 81 | 82 | return nil 83 | } 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Machinist 2 | 3 | Machinist is an application that makes it easy for customers to collect data from their OPC UA data servers and publish the data to Google Cloud IoT Core. Machinist connects to any OPC UA server on your factory's network. You can add metrics for collection at any time through a browser. If internet connectivity is interrupted, Machinist stores metric values for a configurable time period and forwards the data to Google when your connection is restored. Machinist is available for Windows, Linux and MacOS. 4 | 5 | ## A Walkthrough 6 | 7 | A customer visits your website and chooses to trial the service. After accepting the license, the customer downloads two files. The first file 'config.yaml' is unique to the customer. It's contents: 8 | 9 | kind: gateway 10 | projectID: our-chassis-269114 11 | region: us-central1 12 | registryID: golang-iot-test-registry 13 | deviceID: gateway-92d6686e-92de-4c3f-bca2-dea6f054ab24 14 | privateKey: | 15 | -----BEGIN RSA PRIVATE KEY----- 16 | -----END RSA PRIVATE KEY----- 17 | algorithm: RS256 18 | storePath: ./data 19 | 20 | The customer chooses a second file to download, depending on the preferred OS. Example 'machinist-windows-amd64-0.1.0.zip'. 21 | 22 | The customer uncompresses the second file and places it in a directory along with the first file ‘config.yaml’. The customer then starts the Machinist application. 23 | 24 | ## Walkthrough, Part 2 25 | 26 | The customer returns to the website and configures the machinist by adding machines and metrics. 27 | 28 | ### Adding a machine to the machinist 29 | 30 | The customer adds a machine and specifies a name, opc ua endpoint url and various properties that all have reasonable defaults (sampling interval, publishing interval, etc.) 31 | 32 | ### Adding a metric to a machine 33 | 34 | The customer adds a metric to a machine and specifies a name, opc ua nodeID and various properties that all have reasonable defaults. The customer could also browse through the namespace of the opc ua server and select the metrics of interest. 35 | 36 | ## Walkthrough, Part 3 37 | 38 | The Machinist application is always listening for service requests, such as 'Browse', 'Read', 'Write', and 'Call' published by a client application to the Google Cloud IoT 'commands' topic. For example: 39 | 1) A client issues a 'Write' request and publishes it to the command topic of a particular device, i.e. '/devices/\/commands/write-request'. 40 | 2) Machinist receives the request and writes to the OPC UA server on the factory network. 41 | 3) The OPC UA server sends the 'Write' response back to Machinist. 42 | 4) Machinist forwards the response to Google Cloud IoT topic '/devices/\/events/write-response'. 43 | 5) The client reads the response and can check the status code. -------------------------------------------------------------------------------- /spa/src/components/Info.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 98 | 99 | -------------------------------------------------------------------------------- /opcua/write.go: -------------------------------------------------------------------------------- 1 | package opcua 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | ua "github.com/awcullen/opcua" 11 | mqtt "github.com/eclipse/paho.mqtt.golang" 12 | "github.com/pkg/errors" 13 | "google.golang.org/protobuf/proto" 14 | ) 15 | 16 | // onWriteRequest handles protobuf messages published to the 'commands/write-requests' topic of the MQTT broker. 17 | // WriteResponse is published to the 'events/write-requests' topic 18 | func (d *device) onWriteRequest(client mqtt.Client, msg mqtt.Message) { 19 | req := new(WriteRequest) 20 | err := json.Unmarshal(msg.Payload(), req) 21 | if err != nil { 22 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error parsing WriteRequest", d.deviceID)) 23 | return 24 | } 25 | 26 | res, err := d.Write(d.ctx, req) 27 | if err != nil { 28 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error browsing", d.deviceID)) 29 | return 30 | } 31 | 32 | payload, err := proto.Marshal(res) 33 | if err != nil { 34 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error marshalling response", d.deviceID)) 35 | return 36 | } 37 | if tok := client.Publish(fmt.Sprintf("/devices/%s/events/write-response", d.DeviceID()), 1, false, payload); tok.Wait() && tok.Error() != nil { 38 | log.Println(errors.Wrapf(tok.Error(), "[opcua] Device '%s' error publishing to broker", d.deviceID)) 39 | return 40 | } 41 | } 42 | 43 | // Write service grpc implementation. 44 | func (d *device) Write(ctx context.Context, req *WriteRequest) (*WriteResponse, error) { 45 | // prepare request 46 | nodes := make([]*ua.WriteValue, 0) 47 | for _, item := range req.NodesToWrite { 48 | nodes = append(nodes, &ua.WriteValue{ 49 | NodeID: ua.ParseNodeID(item.NodeId), 50 | AttributeID: item.AttributeId, 51 | IndexRange: item.IndexRange, 52 | Value: ua.NewDataValueVariant(toUaVariant(item.Value), 0, time.Time{}, 0, time.Time{}, 0), 53 | }) 54 | } 55 | req2 := &ua.WriteRequest{ 56 | NodesToWrite: nodes, 57 | } 58 | 59 | // do request 60 | res2, err := d.write(ctx, req2) 61 | if err != nil { 62 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' Error writing", d.deviceID)) 63 | res := &WriteResponse{ 64 | RequestHandle: req.RequestHandle, 65 | StatusCode: uint32(ua.BadConnectionClosed), 66 | } 67 | return res, nil 68 | } 69 | 70 | // prepare response 71 | results := make([]uint32, 0) 72 | for _, result := range res2.Results { 73 | results = append(results, uint32(result)) 74 | } 75 | 76 | // return response 77 | res := &WriteResponse{ 78 | RequestHandle: req.RequestHandle, 79 | StatusCode: uint32(res2.ServiceResult), 80 | Results: results, 81 | } 82 | return res, nil 83 | 84 | // return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") 85 | } 86 | 87 | // Write sets the values of Attributes of one or more Nodes. 88 | func (d *device) write(ctx context.Context, request *ua.WriteRequest) (*ua.WriteResponse, error) { 89 | response, err := d.request(ctx, request) 90 | if err != nil { 91 | return nil, err 92 | } 93 | return response.(*ua.WriteResponse), nil 94 | } 95 | -------------------------------------------------------------------------------- /cmd/streamer/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | 12 | "cloud.google.com/go/pubsub" 13 | "github.com/awcullen/machinist/opcua" 14 | "github.com/pkg/errors" 15 | "github.com/spf13/cobra" 16 | "github.com/spf13/viper" 17 | "google.golang.org/protobuf/proto" 18 | ) 19 | 20 | var ( 21 | command = "streamer" 22 | projectID string 23 | subID string 24 | deviceID string 25 | metricID string 26 | version string 27 | commit string 28 | ) 29 | 30 | func main() { 31 | var cmd = &cobra.Command{ 32 | Use: command, 33 | Args: cobra.NoArgs, 34 | Version: fmt.Sprintf("%s+%s", version, commit), 35 | Run: func(cmd *cobra.Command, args []string) { 36 | fmt.Printf("%s %s\n", cmd.Use, cmd.Version) 37 | run() 38 | }, 39 | } 40 | 41 | // Set the command line flags. 42 | setFlags(cmd) 43 | 44 | // Check for environment variables prefixed by the command 45 | // name and referenced by the flag name (eg: FOOCOMMAND_BAR) 46 | // and override flag values before running the main command. 47 | viper.SetEnvPrefix(command) 48 | viper.AutomaticEnv() 49 | viper.BindPFlags(cmd.Flags()) 50 | 51 | cmd.Execute() 52 | } 53 | 54 | func run() { 55 | if projectID == "" { 56 | fmt.Println("Missing parameter 'project'.") 57 | return 58 | } 59 | if subID == "" { 60 | fmt.Println("Missing parameter 'sub'.") 61 | return 62 | } 63 | cctx, cancel := context.WithCancel(context.Background()) 64 | pullMsgs(cctx, projectID, subID, deviceID, metricID) 65 | waitForSignal() 66 | cancel() 67 | time.Sleep(3 * time.Second) 68 | } 69 | 70 | func setFlags(cmd *cobra.Command) { 71 | cmd.Flags().StringVarP(&projectID, "project", "p", "our-chassis-269114", "project id") 72 | cmd.Flags().StringVarP(&subID, "sub", "s", "test-sub12", "subscription id") 73 | cmd.Flags().StringVarP(&deviceID, "device", "d", "", "filter to this device id") 74 | cmd.Flags().StringVarP(&metricID, "metric", "m", "", "filter to this metric id") 75 | } 76 | 77 | func waitForSignal() { 78 | sigs := make(chan os.Signal, 1) 79 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 80 | <-sigs 81 | fmt.Println() 82 | } 83 | 84 | func pullMsgs(ctx context.Context, projectID, subID, deviceID, metricID string) { 85 | client, err := pubsub.NewClient(ctx, projectID) 86 | if err != nil { 87 | log.Println(errors.Wrap(err, "pubsub.NewClient")) 88 | } 89 | sub := client.Subscription(subID) 90 | err = sub.Receive(ctx, func(ctx context.Context, msg *pubsub.Message) { 91 | msg.Ack() 92 | mDeviceID := msg.Attributes["deviceId"] 93 | if deviceID != "" && deviceID != mDeviceID { 94 | return 95 | } 96 | fmt.Println(msg.PublishTime.Format(time.StampMilli)) 97 | notification := &opcua.PublishResponse{} 98 | proto.Unmarshal(msg.Data, notification) 99 | for _, m := range notification.Metrics { 100 | if metricID != "" && metricID != m.Name { 101 | continue 102 | } 103 | fmt.Printf("ts: %d, d: %s, m: %s, v: %+v, q: %#X\n", m.Timestamp, mDeviceID, m.Name, m.Value, m.StatusCode) 104 | } 105 | }) 106 | if err != nil { 107 | log.Println(errors.Wrap(err, "Error receiving")) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /spa/src/components/Home.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 127 | -------------------------------------------------------------------------------- /cmd/machinist/main.go: -------------------------------------------------------------------------------- 1 | //go:generate statik -src=../../spa/dist 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "path" 13 | "syscall" 14 | 15 | _ "expvar" 16 | _ "net/http/pprof" 17 | 18 | _ "github.com/awcullen/machinist/cmd/machinist/statik" 19 | 20 | "github.com/awcullen/machinist/cloud" 21 | "github.com/awcullen/machinist/gateway" 22 | "github.com/gorilla/mux" 23 | "github.com/pkg/errors" 24 | "github.com/rakyll/statik/fs" 25 | "github.com/spf13/cobra" 26 | "github.com/spf13/viper" 27 | "gopkg.in/yaml.v3" 28 | ) 29 | 30 | var ( 31 | command = "machinist" 32 | configFile string 33 | debug bool 34 | serverPort string 35 | version string 36 | commit string 37 | ) 38 | 39 | func main() { 40 | var cmd = &cobra.Command{ 41 | Use: command, 42 | Args: cobra.NoArgs, 43 | Version: fmt.Sprintf("%s+%s", version, commit), 44 | Run: func(cmd *cobra.Command, args []string) { 45 | fmt.Printf("%s %s\n", cmd.Use, cmd.Version) 46 | run() 47 | }, 48 | } 49 | 50 | // Set the command line flags. 51 | setFlags(cmd) 52 | 53 | // Check for environment variables prefixed by the command 54 | // name and referenced by the flag name (eg: FOOCOMMAND_BAR) 55 | // and override flag values before running the main command. 56 | viper.SetEnvPrefix(command) 57 | viper.AutomaticEnv() 58 | viper.BindPFlags(cmd.Flags()) 59 | 60 | cmd.Execute() 61 | } 62 | 63 | func run() { 64 | // mqtt.CRITICAL = log.New(os.Stderr, "", log.LstdFlags) 65 | // mqtt.ERROR = log.New(os.Stderr, "", log.LstdFlags) 66 | // mqtt.WARN = log.New(os.Stderr, "", log.LstdFlags) 67 | // mqtt.DEBUG = log.New(os.Stderr, "", log.LstdFlags) 68 | config := new(cloud.Config) 69 | in, err := ioutil.ReadFile(configFile) 70 | if err != nil { 71 | log.Fatalln(errors.Wrap(err, "[main] Error reading config file")) 72 | } 73 | err = yaml.Unmarshal(in, config) 74 | if err != nil { 75 | log.Fatalln(errors.Wrap(err, "[main] Error unmarshalling from config file")) 76 | } 77 | g, err := gateway.Start(*config) 78 | if err != nil { 79 | log.Fatalln(errors.Wrap(err, "[main] Error starting gateway")) 80 | } 81 | 82 | // Routing 83 | muxrouter := mux.NewRouter() 84 | routes := Routes{ 85 | disableCORS: true, 86 | gateway: g, 87 | } 88 | 89 | // API routes 90 | muxrouter.HandleFunc("/api/home", routes.apiHomeRoute) 91 | muxrouter.HandleFunc("/api/info", routes.apiInfoRoute) 92 | muxrouter.HandleFunc("/api/metrics", routes.apiMetricsRoute) 93 | 94 | // Handle static content 95 | statikFS, err := fs.New() 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | muxrouter.Handle("/", http.FileServer(statikFS)) 100 | 101 | // EVERYTHING redirect to index.html if not found. 102 | muxrouter.NotFoundHandler = http.FileServer(&fsWrapper{statikFS}) 103 | 104 | // Start server 105 | http.ListenAndServe(":"+serverPort, muxrouter) 106 | 107 | // time.Sleep(60 * time.Second) 108 | waitForSignal() 109 | g.Stop() 110 | } 111 | 112 | func setFlags(cmd *cobra.Command) { 113 | cmd.Flags().BoolVarP(&debug, "debug", "d", false, "enable debug output") 114 | cmd.Flags().StringVarP(&configFile, "configFile", "c", "./config.yaml", "config file path") 115 | cmd.Flags().StringVarP(&serverPort, "serverPort", "p", "4000", "http server port") 116 | } 117 | 118 | func waitForSignal() { 119 | sigs := make(chan os.Signal, 1) 120 | signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) 121 | <-sigs 122 | fmt.Println() 123 | } 124 | 125 | type fsWrapper struct { 126 | root http.FileSystem 127 | } 128 | 129 | func (f *fsWrapper) Open(name string) (http.File, error) { 130 | ret, err := f.root.Open(name) 131 | if !os.IsNotExist(err) || path.Ext(name) != "" { 132 | return ret, err 133 | } 134 | return f.root.Open("/index.html") 135 | } 136 | -------------------------------------------------------------------------------- /opcua/monitored-item.go: -------------------------------------------------------------------------------- 1 | package opcua 2 | 3 | import ( 4 | "time" 5 | 6 | "sync" 7 | 8 | ua "github.com/awcullen/opcua" 9 | deque "github.com/gammazero/deque" 10 | ) 11 | 12 | const ( 13 | maxQueueSize = 1024 14 | minSamplingInterval = 100.0 15 | maxSamplingInterval = 60 * 1000.0 16 | ) 17 | 18 | // MonitoredItem specifies the node that is monitored for data changes or events. 19 | type MonitoredItem struct { 20 | sync.RWMutex 21 | metricID string 22 | monitoringMode ua.MonitoringMode 23 | queueSize uint32 24 | currentValue *ua.DataValue 25 | queue deque.Deque 26 | ts time.Time 27 | ti time.Duration 28 | } 29 | 30 | // NewMonitoredItem constructs a new MonitoredItem. 31 | func NewMonitoredItem(metricID string, monitoringMode ua.MonitoringMode, samplingInterval float64, queueSize uint32, ts time.Time) *MonitoredItem { 32 | if samplingInterval > maxSamplingInterval { 33 | samplingInterval = maxSamplingInterval 34 | } 35 | if samplingInterval < minSamplingInterval { 36 | samplingInterval = minSamplingInterval 37 | } 38 | if queueSize > maxQueueSize { 39 | queueSize = maxQueueSize 40 | } 41 | if queueSize < 1 { 42 | queueSize = 1 43 | } 44 | mi := &MonitoredItem{ 45 | metricID: metricID, 46 | monitoringMode: monitoringMode, 47 | queueSize: queueSize, 48 | ti: time.Duration(samplingInterval) * time.Millisecond, 49 | queue: deque.Deque{}, 50 | currentValue: ua.NewDataValueVariant(&ua.NilVariant, ua.BadWaitingForInitialData, time.Time{}, 0, time.Time{}, 0), 51 | ts: ts, 52 | } 53 | return mi 54 | } 55 | 56 | // Enqueue stores the current value of the item. 57 | func (mi *MonitoredItem) Enqueue(value *ua.DataValue) { 58 | mi.Lock() 59 | for mi.queue.Len() >= int(mi.queueSize) { 60 | mi.queue.PopFront() // discard oldest 61 | } 62 | mi.queue.PushBack(value) 63 | mi.Unlock() 64 | } 65 | 66 | // Notifications returns the data values from the queue, up to given time and max count. 67 | func (mi *MonitoredItem) Notifications(tn time.Time, max int) (notifications []*ua.DataValue, more bool) { 68 | mi.Lock() 69 | defer mi.Unlock() 70 | notifications = make([]*ua.DataValue, 0, mi.queueSize) 71 | if mi.monitoringMode != ua.MonitoringModeReporting { 72 | return notifications, false 73 | } 74 | // if in sampling interval mode, append the last value of each sampling interval 75 | if mi.ti > 0 { 76 | if mi.ts.IsZero() { 77 | 78 | } 79 | // for each interval (a tumbling window that ends at ts (the sample time)) 80 | for ; !mi.ts.After(tn) && len(notifications) < max; mi.ts = mi.ts.Add(mi.ti) { 81 | // for each value in queue 82 | for mi.queue.Len() > 0 { 83 | // peek 84 | peek := mi.queue.Front().(*ua.DataValue) 85 | // if peeked value timestamp is before ts (the sample time) 86 | if !peek.ServerTimestamp().After(mi.ts) { 87 | // overwrite current with the peeked value 88 | mi.currentValue = peek 89 | // pop it from the queue 90 | mi.queue.PopFront() 91 | } else { 92 | // peeked value timestamp is later 93 | break 94 | } 95 | } 96 | // update current's timestamp, and append it to the notifications to return 97 | cv := mi.currentValue 98 | mi.currentValue = ua.NewDataValueVariant(cv.InnerVariant(), cv.StatusCode(), cv.SourceTimestamp(), 0, mi.ts, 0) 99 | notifications = append(notifications, mi.currentValue) 100 | } 101 | } else { 102 | // for each value in queue 103 | for mi.queue.Len() > 0 && len(notifications) < max { 104 | peek := mi.queue.Front().(*ua.DataValue) 105 | // if peeked value timestamp is before tn (the sample time) 106 | if !peek.ServerTimestamp().After(tn) { 107 | // append it to the notifications to return 108 | notifications = append(notifications, peek) 109 | mi.currentValue = peek 110 | // pop it from the queue 111 | mi.queue.PopFront() 112 | } else { 113 | // peeked value timestamp is later 114 | break 115 | } 116 | } 117 | } 118 | 119 | more = mi.queue.Len() > 0 120 | return notifications, more 121 | } 122 | -------------------------------------------------------------------------------- /cmd/machinist/routes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "os" 7 | "runtime" 8 | "sort" 9 | "time" 10 | 11 | "github.com/awcullen/machinist/gateway" 12 | "github.com/shirou/gopsutil/cpu" 13 | "github.com/shirou/gopsutil/disk" 14 | "github.com/shirou/gopsutil/mem" 15 | "github.com/shirou/gopsutil/net" 16 | ) 17 | 18 | // SysInfo is generic holder for passsing data back 19 | type SysInfo struct { 20 | Hostname string `json:"hostname"` 21 | OS string `json:"os"` 22 | Arch string `json:"architecture"` 23 | CPUs int `json:"cpuCount"` 24 | GoVersion string `json:"goVersion"` 25 | NetRemoteAddr string `json:"netRemoteAddress"` 26 | NetHost string `json:"netHost"` 27 | EnvVars []string `json:"envVars"` 28 | } 29 | 30 | // Metrics are real time system counters 31 | type Metrics struct { 32 | MemTotal uint64 `json:"memTotal"` 33 | MemUsed uint64 `json:"memUsed"` 34 | CPUPerc float64 `json:"cpuPerc"` 35 | DiskTotal uint64 `json:"diskTotal"` 36 | DiskFree uint64 `json:"diskFree"` 37 | NetBytesSent uint64 `json:"netBytesSent"` 38 | NetBytesRecv uint64 `json:"netBytesRecv"` 39 | } 40 | 41 | // Routes is our exported class 42 | type Routes struct { 43 | disableCORS bool 44 | gateway *gateway.Gateway 45 | } 46 | 47 | // 48 | // /api/info - Return system information and properties 49 | // 50 | func (r Routes) apiInfoRoute(resp http.ResponseWriter, req *http.Request) { 51 | // CORS is for wimps 52 | if r.disableCORS { 53 | resp.Header().Set("Access-Control-Allow-Origin", "*") 54 | } 55 | 56 | var info SysInfo 57 | 58 | // Grab various bits of infomation from where we can 59 | info.Hostname, _ = os.Hostname() 60 | info.GoVersion = runtime.Version() 61 | info.OS = runtime.GOOS 62 | info.Arch = runtime.GOARCH 63 | info.CPUs = runtime.NumCPU() 64 | info.NetRemoteAddr = req.RemoteAddr 65 | info.NetHost = req.Host 66 | info.EnvVars = os.Environ() 67 | 68 | // JSON-ify our info 69 | js, err := json.Marshal(info) 70 | if err != nil { 71 | http.Error(resp, err.Error(), http.StatusInternalServerError) 72 | return 73 | } 74 | 75 | // Fire JSON result back down the internet tubes 76 | resp.Header().Set("Content-Type", "application/json") 77 | resp.Write(js) 78 | } 79 | 80 | // 81 | // /api/metrics - Return system metrics cpu, mem, etc 82 | // 83 | func (r Routes) apiMetricsRoute(resp http.ResponseWriter, req *http.Request) { 84 | // CORS is for wimps 85 | if r.disableCORS { 86 | resp.Header().Set("Access-Control-Allow-Origin", "*") 87 | } 88 | 89 | var metrics Metrics 90 | 91 | // Memory stuff 92 | memStats, err := mem.VirtualMemory() 93 | if err != nil { 94 | http.Error(resp, err.Error(), http.StatusInternalServerError) 95 | return 96 | } 97 | metrics.MemTotal = memStats.Total 98 | metrics.MemUsed = memStats.Used 99 | 100 | // CPU / processor stuff 101 | cpuStats, err := cpu.Percent(time.Millisecond*1000, false) 102 | if err != nil { 103 | http.Error(resp, err.Error(), http.StatusInternalServerError) 104 | return 105 | } 106 | metrics.CPUPerc = cpuStats[0] 107 | 108 | // Disk and filesystem usage stuff 109 | diskStats, err := disk.Usage("/") 110 | if err != nil { 111 | http.Error(resp, err.Error(), http.StatusInternalServerError) 112 | return 113 | } 114 | metrics.DiskTotal = diskStats.Total 115 | metrics.DiskFree = diskStats.Free 116 | 117 | // Network stuff 118 | netStats, err := net.IOCounters(false) 119 | if err != nil { 120 | http.Error(resp, err.Error(), http.StatusInternalServerError) 121 | return 122 | } 123 | metrics.NetBytesRecv = netStats[0].BytesRecv 124 | metrics.NetBytesSent = netStats[0].BytesSent 125 | 126 | // JSON-ify our metrics 127 | js, err := json.Marshal(metrics) 128 | if err != nil { 129 | http.Error(resp, err.Error(), http.StatusInternalServerError) 130 | return 131 | } 132 | 133 | // Fire JSON result back down the internet tubes 134 | resp.Header().Set("Content-Type", "application/json") 135 | resp.Write(js) 136 | } 137 | 138 | // GatewayConfig ... 139 | type GatewayConfig struct { 140 | Name string `json:"name"` 141 | Location string `json:"location"` 142 | ProjectID string `json:"projectId"` 143 | Region string `json:"region"` 144 | RegistryID string `json:"registryId"` 145 | DeviceID string `json:"deviceId"` 146 | StorePath string `json:"storePath"` 147 | Devices []DeviceConfig `json:"devices"` 148 | } 149 | 150 | // DeviceConfig ... 151 | type DeviceConfig struct { 152 | Name string `json:"name"` 153 | Location string `json:"location"` 154 | DeviceID string `json:"deviceId"` 155 | Kind string `json:"kind"` 156 | } 157 | 158 | // GatewayMetrics are real time system counters 159 | type GatewayMetrics struct { 160 | MemTotal uint64 `json:"memTotal"` 161 | MemUsed uint64 `json:"memUsed"` 162 | CPUPerc float64 `json:"cpuPerc"` 163 | DiskTotal uint64 `json:"diskTotal"` 164 | DiskFree uint64 `json:"diskFree"` 165 | NetBytesSent uint64 `json:"netBytesSent"` 166 | NetBytesRecv uint64 `json:"netBytesRecv"` 167 | } 168 | 169 | // 170 | // /api/gateway - Return gateway metrics 171 | // 172 | func (r Routes) apiHomeRoute(resp http.ResponseWriter, req *http.Request) { 173 | // CORS is for wimps 174 | if r.disableCORS { 175 | resp.Header().Set("Access-Control-Allow-Origin", "*") 176 | } 177 | 178 | g := r.gateway 179 | devices := []DeviceConfig{} 180 | for _, d := range g.Devices() { 181 | dc := DeviceConfig{ 182 | Name: d.Name(), 183 | Location: d.Location(), 184 | DeviceID: d.DeviceID(), 185 | Kind: d.Kind(), 186 | } 187 | devices = append(devices, dc) 188 | } 189 | sort.Slice(devices, func(i, j int) bool { 190 | return devices[i].Name < devices[j].Name 191 | }) 192 | config := &GatewayConfig{ 193 | Name: g.Name(), 194 | Location: g.Location(), 195 | ProjectID: g.ProjectID(), 196 | Region: g.Region(), 197 | RegistryID: g.RegistryID(), 198 | DeviceID: g.DeviceID(), 199 | StorePath: g.StorePath(), 200 | Devices: devices, 201 | } 202 | 203 | // JSON-ify our metrics 204 | js, err := json.Marshal(config) 205 | if err != nil { 206 | http.Error(resp, err.Error(), http.StatusInternalServerError) 207 | return 208 | } 209 | // Fire JSON result back out the internet tubes 210 | resp.Header().Set("Content-Type", "application/json") 211 | resp.Write(js) 212 | } 213 | -------------------------------------------------------------------------------- /opcua/browse.go: -------------------------------------------------------------------------------- 1 | package opcua 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | 8 | ua "github.com/awcullen/opcua" 9 | mqtt "github.com/eclipse/paho.mqtt.golang" 10 | "github.com/pkg/errors" 11 | "google.golang.org/protobuf/proto" 12 | ) 13 | 14 | const ( 15 | // defaultTimeoutHint is the default number of milliseconds before a request is cancelled. (15 sec) 16 | defaultTimeoutHint uint32 = 15000 17 | // defaultDiagnosticsHint is the default diagnostic hint that is sent in a request. (None) 18 | defaultDiagnosticsHint uint32 = 0x00000000 19 | ) 20 | 21 | // onBrowseRequest handles messages to the 'commands/browse-requests' topic of the MQTT broker. 22 | func (d *device) onBrowseRequest(client mqtt.Client, msg mqtt.Message) { 23 | req := new(BrowseRequest) 24 | err := proto.Unmarshal(msg.Payload(), req) 25 | if err != nil { 26 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error parsing browse-request", d.deviceID)) 27 | return 28 | } 29 | 30 | res, err := d.Browse(d.ctx, req) 31 | if err != nil { 32 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error browsing", d.deviceID)) 33 | return 34 | } 35 | 36 | payload, err := proto.Marshal(res) 37 | if err != nil { 38 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error marshalling response", d.deviceID)) 39 | return 40 | } 41 | if tok := client.Publish(fmt.Sprintf("/devices/%s/events/browse-response", d.DeviceID()), 1, false, payload); tok.Wait() && tok.Error() != nil { 42 | log.Println(errors.Wrapf(tok.Error(), "[opcua] Device '%s' error publishing to broker", d.deviceID)) 43 | return 44 | } 45 | } 46 | 47 | // Browse grpc service implementation. 48 | func (d *device) Browse(ctx context.Context, req *BrowseRequest) (*BrowseResponse, error) { 49 | // prepare request 50 | nodes := make([]*ua.BrowseDescription, 0) 51 | for _, item := range req.NodesToBrowse { 52 | nodes = append(nodes, &ua.BrowseDescription{ 53 | NodeID: ua.ParseNodeID(item.NodeId), 54 | BrowseDirection: ua.BrowseDirection(item.BrowseDirection), 55 | ReferenceTypeID: ua.ParseNodeID(item.ReferenceTypeId), 56 | IncludeSubtypes: item.IncludeSubtypes, 57 | NodeClassMask: item.NodeClassMask, 58 | ResultMask: item.ResultMask, 59 | }) 60 | } 61 | req2 := &ua.BrowseRequest{ 62 | RequestedMaxReferencesPerNode: req.RequestedMaxReferencesPerNode, 63 | NodesToBrowse: nodes, 64 | } 65 | 66 | // do request 67 | res2, err := d.browse(ctx, req2) 68 | if err != nil { 69 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error browsing", d.deviceID)) 70 | res := &BrowseResponse{ 71 | RequestHandle: req.RequestHandle, 72 | StatusCode: uint32(ua.BadConnectionClosed), 73 | } 74 | return res, nil 75 | } 76 | 77 | // prepare response 78 | results := make([]*BrowseResult, 0) 79 | for _, result := range res2.Results { 80 | refs := make([]*ReferenceDescription, 0) 81 | for _, ref := range result.References { 82 | refs = append(refs, &ReferenceDescription{ 83 | ReferenceTypeId: ref.ReferenceTypeID.String(), 84 | IsForward: ref.IsForward, 85 | NodeId: ref.NodeID.String(), 86 | BrowseName: ref.BrowseName.String(), 87 | DisplayName: ref.DisplayName.Text, 88 | NodeClass: uint32(ref.NodeClass), 89 | TypeDefinition: ref.TypeDefinition.String(), 90 | }) 91 | } 92 | results = append(results, &BrowseResult{ 93 | StatusCode: uint32(result.StatusCode), 94 | ContinuationPoint: []byte(result.ContinuationPoint), 95 | References: refs, 96 | }) 97 | } 98 | 99 | // return response 100 | res := &BrowseResponse{ 101 | RequestHandle: req.RequestHandle, 102 | StatusCode: uint32(res2.ServiceResult), 103 | Results: results, 104 | } 105 | return res, nil 106 | } 107 | 108 | // Browse discovers the References of a specified Node. 109 | func (d *device) browse(ctx context.Context, request *ua.BrowseRequest) (*ua.BrowseResponse, error) { 110 | response, err := d.request(ctx, request) 111 | if err != nil { 112 | return nil, err 113 | } 114 | return response.(*ua.BrowseResponse), nil 115 | } 116 | 117 | // onBrowseNextRequest handles messages to the 'commands/browse-next-requests' topic of the MQTT broker. 118 | func (d *device) onBrowseNextRequest(client mqtt.Client, msg mqtt.Message) { 119 | req := new(BrowseNextRequest) 120 | err := proto.Unmarshal(msg.Payload(), req) 121 | if err != nil { 122 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error parsing browse-next-request", d.deviceID)) 123 | return 124 | } 125 | 126 | res, err := d.BrowseNext(d.ctx, req) 127 | if err != nil { 128 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error browsing", d.deviceID)) 129 | return 130 | } 131 | 132 | payload, err := proto.Marshal(res) 133 | if err != nil { 134 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error marshalling response", d.deviceID)) 135 | return 136 | } 137 | if tok := client.Publish(fmt.Sprintf("/devices/%s/events/browse-next-response", d.DeviceID()), 1, false, payload); tok.Wait() && tok.Error() != nil { 138 | log.Println(errors.Wrapf(tok.Error(), "[opcua] Device '%s' error publishing to broker", d.deviceID)) 139 | return 140 | } 141 | } 142 | 143 | // BrowseNext service implementation. 144 | func (d *device) BrowseNext(ctx context.Context, req *BrowseNextRequest) (*BrowseNextResponse, error) { 145 | // prepare request 146 | cps := make([]ua.ByteString, 0) 147 | for _, item := range req.ContinuationPoints { 148 | cps = append(cps, ua.ByteString(item)) 149 | } 150 | req2 := &ua.BrowseNextRequest{ 151 | ReleaseContinuationPoints: req.ReleaseContinuationPoints, 152 | ContinuationPoints: cps, 153 | } 154 | 155 | // do request 156 | res2, err := d.browseNext(ctx, req2) 157 | if err != nil { 158 | log.Println(errors.Wrapf(err, "[opcua] Device '%s' error browsing", d.deviceID)) 159 | res := &BrowseNextResponse{ 160 | RequestHandle: req.RequestHandle, 161 | StatusCode: uint32(ua.BadConnectionClosed), 162 | } 163 | return res, nil 164 | } 165 | 166 | // prepare response 167 | results := make([]*BrowseResult, 0) 168 | for _, result := range res2.Results { 169 | refs := make([]*ReferenceDescription, 0) 170 | for _, ref := range result.References { 171 | refs = append(refs, &ReferenceDescription{ 172 | ReferenceTypeId: ref.ReferenceTypeID.String(), 173 | IsForward: ref.IsForward, 174 | NodeId: ref.NodeID.String(), 175 | BrowseName: ref.BrowseName.String(), 176 | DisplayName: ref.DisplayName.Text, 177 | NodeClass: uint32(ref.NodeClass), 178 | TypeDefinition: ref.TypeDefinition.String(), 179 | }) 180 | } 181 | results = append(results, &BrowseResult{ 182 | StatusCode: uint32(result.StatusCode), 183 | ContinuationPoint: []byte(result.ContinuationPoint), 184 | References: refs, 185 | }) 186 | } 187 | 188 | // return response 189 | res := &BrowseNextResponse{ 190 | RequestHandle: req.RequestHandle, 191 | StatusCode: uint32(res2.ServiceResult), 192 | Results: results, 193 | } 194 | return res, nil 195 | } 196 | 197 | // BrowseNext requests the next set of Browse responses, when the information is too large to be sent in a single response. 198 | func (d *device) browseNext(ctx context.Context, request *ua.BrowseNextRequest) (*ua.BrowseNextResponse, error) { 199 | response, err := d.request(ctx, request) 200 | if err != nil { 201 | return nil, err 202 | } 203 | return response.(*ua.BrowseNextResponse), nil 204 | } 205 | -------------------------------------------------------------------------------- /device2_config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Line2 3 | description: Building1 4 | endpointURL: opc.tcp://127.0.0.1:48010 5 | securityPolicy: http://opcfoundation.org/UA/SecurityPolicy#None 6 | publishInterval: 5000.0 7 | samplingInterval: 1000.0 8 | queueSize: 16 9 | metrics: 10 | - metricID: 104adf16 11 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0000 12 | - metricID: c0ab6f68 13 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0001 14 | - metricID: 724a5614 15 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0002 16 | - metricID: 32e6a68f 17 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0003 18 | - metricID: 29dd9dd3 19 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0004 20 | - metricID: 7bd4abe6 21 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0005 22 | - metricID: 8e77a83c 23 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0006 24 | - metricID: 43745d85 25 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0007 26 | - metricID: 16f1f026 27 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0008 28 | - metricID: 58d82b83 29 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0009 30 | - metricID: d1888cac 31 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0010 32 | - metricID: 5681d463 33 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0011 34 | - metricID: 57b03cbe 35 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0012 36 | - metricID: 5ed0014e 37 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0013 38 | - metricID: 4bb7393f 39 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0014 40 | - metricID: 7487e4de 41 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0015 42 | - metricID: 38a9b5da 43 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0016 44 | - metricID: 083e7efb 45 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0017 46 | - metricID: 01b4a92e 47 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0018 48 | - metricID: 472b2d8e 49 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0019 50 | - metricID: 7aa7f90a 51 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0020 52 | - metricID: 61535b1a 53 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0021 54 | - metricID: 357d124b 55 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0022 56 | - metricID: "88589e19" 57 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0023 58 | - metricID: 790f15ba 59 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0024 60 | - metricID: 0cc6c9b1 61 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0025 62 | - metricID: 99f3528e 63 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0026 64 | - metricID: 67f6eba9 65 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0027 66 | - metricID: fcd163ce 67 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0028 68 | - metricID: db25abe5 69 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0029 70 | - metricID: 71e99f3c 71 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0030 72 | - metricID: 982c661a 73 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0031 74 | - metricID: 44cda9d7 75 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0032 76 | - metricID: 36dee51c 77 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0033 78 | - metricID: 5a5c709c 79 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0034 80 | - metricID: 1d2d2823 81 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0035 82 | - metricID: d4ab720a 83 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0036 84 | - metricID: 5f114576 85 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0037 86 | - metricID: c237b041 87 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0038 88 | - metricID: 12e07f67 89 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0039 90 | - metricID: 167c55a0 91 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0040 92 | - metricID: ce6de02a 93 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0041 94 | - metricID: 26b51746 95 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0042 96 | - metricID: 29ada245 97 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0043 98 | - metricID: ad9baec1 99 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0044 100 | - metricID: f2a36e6b 101 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0045 102 | - metricID: c454a18a 103 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0046 104 | - metricID: e4feba9d 105 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0047 106 | - metricID: a3a142bf 107 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0048 108 | - metricID: 2934a291 109 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0049 110 | - metricID: 83bb5ae0 111 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0050 112 | - metricID: 2bd882ba 113 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0051 114 | - metricID: "94802667" 115 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0052 116 | - metricID: 94458ad9 117 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0053 118 | - metricID: 30dac08d 119 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0054 120 | - metricID: 4602d572 121 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0055 122 | - metricID: 681bc79e 123 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0056 124 | - metricID: 5c3d8131 125 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0057 126 | - metricID: 6d6fed7b 127 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0058 128 | - metricID: 7d6bd4a2 129 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0059 130 | - metricID: f0464245 131 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0060 132 | - metricID: ad4d436f 133 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0061 134 | - metricID: 7f11167a 135 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0062 136 | - metricID: 179678d8 137 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0063 138 | - metricID: 86201ff6 139 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0064 140 | - metricID: 8081d552 141 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0065 142 | - metricID: 46915ea5 143 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0066 144 | - metricID: 597a178e 145 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0067 146 | - metricID: 5b9fee42 147 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0068 148 | - metricID: b33da3b2 149 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0069 150 | - metricID: be260a7c 151 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0070 152 | - metricID: 88a122ac 153 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0071 154 | - metricID: 6a857306 155 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0072 156 | - metricID: a285776a 157 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0073 158 | - metricID: afb5fdbe 159 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0074 160 | - metricID: a1fe8199 161 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0075 162 | - metricID: a8a5079c 163 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0076 164 | - metricID: 0bb4f2ce 165 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0077 166 | - metricID: be37eb0d 167 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0078 168 | - metricID: 9866f361 169 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0079 170 | - metricID: 146a6c39 171 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0080 172 | - metricID: 1745afd1 173 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0081 174 | - metricID: 24f362c1 175 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0082 176 | - metricID: 7f9e91bf 177 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0083 178 | - metricID: 1521d067 179 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0084 180 | - metricID: 9f35e3ff 181 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0085 182 | - metricID: 206c2a14 183 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0086 184 | - metricID: 576f6e6a 185 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0087 186 | - metricID: 0029e2e2 187 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0088 188 | - metricID: 636e5aa8 189 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0089 190 | - metricID: 5d4be091 191 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0090 192 | - metricID: 1fb4160c 193 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0091 194 | - metricID: 5378e619 195 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0092 196 | - metricID: 7ac87965 197 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0093 198 | - metricID: d7ac3925 199 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0094 200 | - metricID: e55bd7d8 201 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0095 202 | - metricID: eabb0036 203 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0096 204 | - metricID: f72dd5ad 205 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0097 206 | - metricID: 9901a4c6 207 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0098 208 | - metricID: e300221a 209 | nodeID: ns=2;s=Demo.Massfolder_Dynamic.Variable0099 210 | -------------------------------------------------------------------------------- /spa/src/assets/go.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /opcua/variant.go: -------------------------------------------------------------------------------- 1 | package opcua 2 | 3 | import ( 4 | "time" 5 | 6 | ua "github.com/awcullen/opcua" 7 | uuid "github.com/google/uuid" 8 | "google.golang.org/protobuf/types/known/timestamppb" 9 | ) 10 | 11 | // toUaVariant coerces the local Variant to ua.Variant 12 | func toUaVariant(value *Variant) *ua.Variant { 13 | switch x := value.GetValue().(type) { 14 | case *Variant_Null: 15 | return &ua.NilVariant 16 | case *Variant_Boolean: 17 | return ua.NewVariantBoolean(x.Boolean) 18 | case *Variant_SByte: 19 | return ua.NewVariantSByte(int8(x.SByte)) 20 | case *Variant_Byte: 21 | return ua.NewVariantByte(uint8(x.Byte)) 22 | case *Variant_Int16: 23 | return ua.NewVariantInt16(int16(x.Int16)) 24 | case *Variant_UInt16: 25 | return ua.NewVariantUInt16(uint16(x.UInt16)) 26 | case *Variant_Int32: 27 | return ua.NewVariantInt32(x.Int32) 28 | case *Variant_UInt32: 29 | return ua.NewVariantUInt32(x.UInt32) 30 | case *Variant_Int64: 31 | return ua.NewVariantInt64(x.Int64) 32 | case *Variant_UInt64: 33 | return ua.NewVariantUInt64(x.UInt64) 34 | case *Variant_Float: 35 | return ua.NewVariantFloat(x.Float) 36 | case *Variant_Double: 37 | return ua.NewVariantDouble(x.Double) 38 | case *Variant_String_: 39 | return ua.NewVariantString(x.String_) 40 | case *Variant_DateTime: 41 | return ua.NewVariantDateTime(time.Unix(x.DateTime.Seconds, int64(x.DateTime.Nanos)).UTC()) 42 | case *Variant_Guid: 43 | y, _ := uuid.FromBytes(x.Guid) 44 | return ua.NewVariantGUID(y) 45 | case *Variant_ByteString: 46 | return ua.NewVariantByteString(ua.ByteString(x.ByteString)) 47 | case *Variant_BooleanArray: 48 | return ua.NewVariantBooleanArray(x.BooleanArray.Value) 49 | case *Variant_SByteArray: 50 | a := make([]int8, len(x.SByteArray.Value)) 51 | for i, v := range x.SByteArray.Value { 52 | a[i] = int8(v) 53 | } 54 | return ua.NewVariantSByteArray(a) 55 | case *Variant_ByteArray: 56 | a := make([]uint8, len(x.ByteArray.Value)) 57 | for i, v := range x.ByteArray.Value { 58 | a[i] = uint8(v) 59 | } 60 | return ua.NewVariantByteArray(a) 61 | case *Variant_Int16Array: 62 | a := make([]int16, len(x.Int16Array.Value)) 63 | for i, v := range x.Int16Array.Value { 64 | a[i] = int16(v) 65 | } 66 | return ua.NewVariantInt16Array(a) 67 | case *Variant_UInt16Array: 68 | a := make([]uint16, len(x.UInt16Array.Value)) 69 | for i, v := range x.UInt16Array.Value { 70 | a[i] = uint16(v) 71 | } 72 | return ua.NewVariantUInt16Array(a) 73 | case *Variant_Int32Array: 74 | return ua.NewVariantInt32Array(x.Int32Array.Value) 75 | case *Variant_UInt32Array: 76 | return ua.NewVariantUInt32Array(x.UInt32Array.Value) 77 | case *Variant_Int64Array: 78 | return ua.NewVariantInt64Array(x.Int64Array.Value) 79 | case *Variant_UInt64Array: 80 | return ua.NewVariantUInt64Array(x.UInt64Array.Value) 81 | case *Variant_FloatArray: 82 | return ua.NewVariantFloatArray(x.FloatArray.Value) 83 | case *Variant_DoubleArray: 84 | return ua.NewVariantDoubleArray(x.DoubleArray.Value) 85 | case *Variant_StringArray: 86 | return ua.NewVariantStringArray(x.StringArray.Value) 87 | case *Variant_DateTimeArray: 88 | a := make([]time.Time, len(x.DateTimeArray.Value)) 89 | for i, v := range x.DateTimeArray.Value { 90 | a[i] = time.Unix(v.Seconds, int64(v.Nanos)).UTC() 91 | } 92 | return ua.NewVariantDateTimeArray(a) 93 | case *Variant_GuidArray: 94 | a := make([]uuid.UUID, len(x.GuidArray.Value)) 95 | for i, v := range x.GuidArray.Value { 96 | a[i], _ = uuid.FromBytes(v) 97 | } 98 | return ua.NewVariantGUIDArray(a) 99 | case *Variant_ByteStringArray: 100 | a := make([]ua.ByteString, len(x.ByteStringArray.Value)) 101 | for i, v := range x.ByteStringArray.Value { 102 | a[i] = ua.ByteString(v) 103 | } 104 | return ua.NewVariantByteStringArray(a) 105 | default: 106 | return &ua.NilVariant 107 | } 108 | } 109 | 110 | // toVariant coerces the ua.Variant to Variant 111 | func toVariant(value *ua.Variant) *Variant { 112 | if len(value.ArrayDimensions()) == 0 { 113 | switch value.Type() { 114 | case ua.VariantTypeNull: 115 | return &Variant{Value: &Variant_Null{}} 116 | case ua.VariantTypeBoolean: 117 | return &Variant{Value: &Variant_Boolean{Boolean: value.Value().(bool)}} 118 | case ua.VariantTypeSByte: 119 | return &Variant{Value: &Variant_SByte{SByte: int32(value.Value().(int8))}} 120 | case ua.VariantTypeByte: 121 | return &Variant{Value: &Variant_Byte{Byte: uint32(value.Value().(uint8))}} 122 | case ua.VariantTypeInt16: 123 | return &Variant{Value: &Variant_Int16{Int16: int32(value.Value().(int16))}} 124 | case ua.VariantTypeUInt16: 125 | return &Variant{Value: &Variant_UInt16{UInt16: uint32(value.Value().(uint16))}} 126 | case ua.VariantTypeInt32: 127 | return &Variant{Value: &Variant_Int32{Int32: value.Value().(int32)}} 128 | case ua.VariantTypeUInt32: 129 | return &Variant{Value: &Variant_UInt32{UInt32: value.Value().(uint32)}} 130 | case ua.VariantTypeInt64: 131 | return &Variant{Value: &Variant_Int64{Int64: value.Value().(int64)}} 132 | case ua.VariantTypeUInt64: 133 | return &Variant{Value: &Variant_UInt64{UInt64: value.Value().(uint64)}} 134 | case ua.VariantTypeFloat: 135 | return &Variant{Value: &Variant_Float{Float: value.Value().(float32)}} 136 | case ua.VariantTypeDouble: 137 | return &Variant{Value: &Variant_Double{Double: value.Value().(float64)}} 138 | case ua.VariantTypeString: 139 | return &Variant{Value: &Variant_String_{String_: value.Value().(string)}} 140 | case ua.VariantTypeDateTime: 141 | x := value.Value().(time.Time) 142 | return &Variant{Value: &Variant_DateTime{DateTime: ×tamppb.Timestamp{Seconds: int64(x.Second()), Nanos: int32(x.Nanosecond())}}} 143 | case ua.VariantTypeGUID: 144 | x := value.Value().(uuid.UUID) 145 | return &Variant{Value: &Variant_Guid{Guid: x[:]}} 146 | case ua.VariantTypeByteString: 147 | return &Variant{Value: &Variant_ByteString{ByteString: value.Value().([]byte)}} 148 | default: 149 | return &Variant{Value: &Variant_Null{}} 150 | } 151 | } 152 | /* 153 | case ua.VariantTypeBooleanArray: 154 | return ua.NewVariantWithType(x.BooleanArray.Value, ua.VariantTypeBoolean, []int32{int32(len(x.BooleanArray.Value))}) 155 | case ua.VariantTypeSByteArray: 156 | a := make([]int8, len(x.SByteArray.Value)) 157 | for i, v := range x.SByteArray.Value { 158 | a[i] = int8(v) 159 | } 160 | return ua.NewVariantWithType(a, ua.VariantTypeSByte, []int32{int32(len(x.SByteArray.Value))}) 161 | case *Variant_ByteArray: 162 | a := make([]uint8, len(x.ByteArray.Value)) 163 | for i, v := range x.ByteArray.Value { 164 | a[i] = uint8(v) 165 | } 166 | return ua.NewVariantWithType(a, ua.VariantTypeByte, []int32{int32(len(x.ByteArray.Value))}) 167 | case *Variant_Int16Array: 168 | a := make([]int16, len(x.Int16Array.Value)) 169 | for i, v := range x.Int16Array.Value { 170 | a[i] = int16(v) 171 | } 172 | return ua.NewVariantWithType(a, ua.VariantTypeInt16, []int32{int32(len(x.Int16Array.Value))}) 173 | case *Variant_UInt16Array: 174 | a := make([]uint16, len(x.UInt16Array.Value)) 175 | for i, v := range x.UInt16Array.Value { 176 | a[i] = uint16(v) 177 | } 178 | return ua.NewVariantWithType(a, ua.VariantTypeUInt16, []int32{int32(len(x.UInt16Array.Value))}) 179 | case *Variant_Int32Array: 180 | return ua.NewVariantWithType(x.Int32Array.Value, ua.VariantTypeInt32, []int32{int32(len(x.Int32Array.Value))}) 181 | case *Variant_UInt32Array: 182 | return ua.NewVariantWithType(x.UInt32Array.Value, ua.VariantTypeUInt32, []int32{int32(len(x.UInt32Array.Value))}) 183 | case *Variant_Int64Array: 184 | return ua.NewVariantWithType(x.Int64Array.Value, ua.VariantTypeInt64, []int32{int32(len(x.Int64Array.Value))}) 185 | case *Variant_UInt64Array: 186 | return ua.NewVariantWithType(x.UInt64Array.Value, ua.VariantTypeUInt64, []int32{int32(len(x.UInt64Array.Value))}) 187 | case *Variant_FloatArray: 188 | return ua.NewVariantWithType(x.FloatArray.Value, ua.VariantTypeFloat, []int32{int32(len(x.FloatArray.Value))}) 189 | case *Variant_DoubleArray: 190 | return ua.NewVariantWithType(x.DoubleArray.Value, ua.VariantTypeDouble, []int32{int32(len(x.DoubleArray.Value))}) 191 | case *Variant_StringArray: 192 | return ua.NewVariantWithType(x.StringArray.Value, ua.VariantTypeString, []int32{int32(len(x.StringArray.Value))}) 193 | case *Variant_DateTimeArray: 194 | a := make([]time.Time, len(x.DateTimeArray.Value)) 195 | for i, v := range x.DateTimeArray.Value { 196 | a[i] = time.Unix(v.Seconds, int64(v.Nanos)).UTC() 197 | } 198 | return ua.NewVariantWithType(a, ua.VariantTypeDateTime, []int32{int32(len(x.DateTimeArray.Value))}) 199 | case *Variant_GuidArray: 200 | a := make([]uuid.UUID, len(x.GuidArray.Value)) 201 | for i, v := range x.GuidArray.Value { 202 | a[i], _ = uuid.FromBytes(v) 203 | } 204 | return ua.NewVariantWithType(a, ua.VariantTypeGuid, []int32{int32(len(x.GuidArray.Value))}) 205 | case *Variant_ByteStringArray: 206 | a := make([]ua.ByteString, len(x.ByteStringArray.Value)) 207 | for i, v := range x.ByteStringArray.Value { 208 | a[i] = ua.ByteString(v) 209 | } 210 | return ua.NewVariantWithType(a, ua.VariantTypeByteString, []int32{int32(len(x.ByteStringArray.Value))}) 211 | default: 212 | return &ua.NilVariant 213 | } 214 | */ 215 | return &Variant{Value: &Variant_Null{}} 216 | 217 | } 218 | -------------------------------------------------------------------------------- /opcua/publish.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.20.1 4 | // protoc v3.11.4 5 | // source: opcua/publish.proto 6 | 7 | package opcua 8 | 9 | import ( 10 | proto "github.com/golang/protobuf/proto" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // This is a compile-time assertion that a sufficiently up-to-date version 25 | // of the legacy proto package is being used. 26 | const _ = proto.ProtoPackageIsVersion4 27 | 28 | // MetricValue message 29 | type MetricValue struct { 30 | state protoimpl.MessageState 31 | sizeCache protoimpl.SizeCache 32 | unknownFields protoimpl.UnknownFields 33 | 34 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 35 | Value *Variant `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"` 36 | StatusCode uint32 `protobuf:"varint,3,opt,name=statusCode,proto3" json:"statusCode,omitempty"` 37 | Timestamp int64 `protobuf:"varint,4,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 38 | } 39 | 40 | func (x *MetricValue) Reset() { 41 | *x = MetricValue{} 42 | if protoimpl.UnsafeEnabled { 43 | mi := &file_opcua_publish_proto_msgTypes[0] 44 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 45 | ms.StoreMessageInfo(mi) 46 | } 47 | } 48 | 49 | func (x *MetricValue) String() string { 50 | return protoimpl.X.MessageStringOf(x) 51 | } 52 | 53 | func (*MetricValue) ProtoMessage() {} 54 | 55 | func (x *MetricValue) ProtoReflect() protoreflect.Message { 56 | mi := &file_opcua_publish_proto_msgTypes[0] 57 | if protoimpl.UnsafeEnabled && x != nil { 58 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 59 | if ms.LoadMessageInfo() == nil { 60 | ms.StoreMessageInfo(mi) 61 | } 62 | return ms 63 | } 64 | return mi.MessageOf(x) 65 | } 66 | 67 | // Deprecated: Use MetricValue.ProtoReflect.Descriptor instead. 68 | func (*MetricValue) Descriptor() ([]byte, []int) { 69 | return file_opcua_publish_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | func (x *MetricValue) GetName() string { 73 | if x != nil { 74 | return x.Name 75 | } 76 | return "" 77 | } 78 | 79 | func (x *MetricValue) GetValue() *Variant { 80 | if x != nil { 81 | return x.Value 82 | } 83 | return nil 84 | } 85 | 86 | func (x *MetricValue) GetStatusCode() uint32 { 87 | if x != nil { 88 | return x.StatusCode 89 | } 90 | return 0 91 | } 92 | 93 | func (x *MetricValue) GetTimestamp() int64 { 94 | if x != nil { 95 | return x.Timestamp 96 | } 97 | return 0 98 | } 99 | 100 | // PublishResponse message 101 | type PublishResponse struct { 102 | state protoimpl.MessageState 103 | sizeCache protoimpl.SizeCache 104 | unknownFields protoimpl.UnknownFields 105 | 106 | Timestamp int64 `protobuf:"varint,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 107 | Metrics []*MetricValue `protobuf:"bytes,2,rep,name=metrics,proto3" json:"metrics,omitempty"` 108 | } 109 | 110 | func (x *PublishResponse) Reset() { 111 | *x = PublishResponse{} 112 | if protoimpl.UnsafeEnabled { 113 | mi := &file_opcua_publish_proto_msgTypes[1] 114 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 115 | ms.StoreMessageInfo(mi) 116 | } 117 | } 118 | 119 | func (x *PublishResponse) String() string { 120 | return protoimpl.X.MessageStringOf(x) 121 | } 122 | 123 | func (*PublishResponse) ProtoMessage() {} 124 | 125 | func (x *PublishResponse) ProtoReflect() protoreflect.Message { 126 | mi := &file_opcua_publish_proto_msgTypes[1] 127 | if protoimpl.UnsafeEnabled && x != nil { 128 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 129 | if ms.LoadMessageInfo() == nil { 130 | ms.StoreMessageInfo(mi) 131 | } 132 | return ms 133 | } 134 | return mi.MessageOf(x) 135 | } 136 | 137 | // Deprecated: Use PublishResponse.ProtoReflect.Descriptor instead. 138 | func (*PublishResponse) Descriptor() ([]byte, []int) { 139 | return file_opcua_publish_proto_rawDescGZIP(), []int{1} 140 | } 141 | 142 | func (x *PublishResponse) GetTimestamp() int64 { 143 | if x != nil { 144 | return x.Timestamp 145 | } 146 | return 0 147 | } 148 | 149 | func (x *PublishResponse) GetMetrics() []*MetricValue { 150 | if x != nil { 151 | return x.Metrics 152 | } 153 | return nil 154 | } 155 | 156 | var File_opcua_publish_proto protoreflect.FileDescriptor 157 | 158 | var file_opcua_publish_proto_rawDesc = []byte{ 159 | 0x0a, 0x13, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x2f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x73, 0x68, 0x2e, 160 | 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x1a, 0x13, 0x6f, 0x70, 161 | 0x63, 0x75, 0x61, 0x2f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 162 | 0x6f, 0x22, 0x85, 0x01, 0x0a, 0x0b, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 163 | 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 164 | 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 165 | 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x2e, 0x56, 0x61, 0x72, 166 | 0x69, 0x61, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 167 | 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 168 | 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x74, 169 | 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 170 | 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x5d, 0x0a, 0x0f, 0x50, 0x75, 0x62, 171 | 0x6c, 0x69, 0x73, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c, 0x0a, 0x09, 172 | 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 173 | 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x2c, 0x0a, 0x07, 0x6d, 0x65, 174 | 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x6f, 0x70, 175 | 0x63, 0x75, 0x61, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 176 | 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x6f, 0x70, 177 | 0x63, 0x75, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 178 | } 179 | 180 | var ( 181 | file_opcua_publish_proto_rawDescOnce sync.Once 182 | file_opcua_publish_proto_rawDescData = file_opcua_publish_proto_rawDesc 183 | ) 184 | 185 | func file_opcua_publish_proto_rawDescGZIP() []byte { 186 | file_opcua_publish_proto_rawDescOnce.Do(func() { 187 | file_opcua_publish_proto_rawDescData = protoimpl.X.CompressGZIP(file_opcua_publish_proto_rawDescData) 188 | }) 189 | return file_opcua_publish_proto_rawDescData 190 | } 191 | 192 | var file_opcua_publish_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 193 | var file_opcua_publish_proto_goTypes = []interface{}{ 194 | (*MetricValue)(nil), // 0: opcua.MetricValue 195 | (*PublishResponse)(nil), // 1: opcua.PublishResponse 196 | (*Variant)(nil), // 2: opcua.Variant 197 | } 198 | var file_opcua_publish_proto_depIdxs = []int32{ 199 | 2, // 0: opcua.MetricValue.value:type_name -> opcua.Variant 200 | 0, // 1: opcua.PublishResponse.metrics:type_name -> opcua.MetricValue 201 | 2, // [2:2] is the sub-list for method output_type 202 | 2, // [2:2] is the sub-list for method input_type 203 | 2, // [2:2] is the sub-list for extension type_name 204 | 2, // [2:2] is the sub-list for extension extendee 205 | 0, // [0:2] is the sub-list for field type_name 206 | } 207 | 208 | func init() { file_opcua_publish_proto_init() } 209 | func file_opcua_publish_proto_init() { 210 | if File_opcua_publish_proto != nil { 211 | return 212 | } 213 | file_opcua_variant_proto_init() 214 | if !protoimpl.UnsafeEnabled { 215 | file_opcua_publish_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 216 | switch v := v.(*MetricValue); i { 217 | case 0: 218 | return &v.state 219 | case 1: 220 | return &v.sizeCache 221 | case 2: 222 | return &v.unknownFields 223 | default: 224 | return nil 225 | } 226 | } 227 | file_opcua_publish_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 228 | switch v := v.(*PublishResponse); i { 229 | case 0: 230 | return &v.state 231 | case 1: 232 | return &v.sizeCache 233 | case 2: 234 | return &v.unknownFields 235 | default: 236 | return nil 237 | } 238 | } 239 | } 240 | type x struct{} 241 | out := protoimpl.TypeBuilder{ 242 | File: protoimpl.DescBuilder{ 243 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 244 | RawDescriptor: file_opcua_publish_proto_rawDesc, 245 | NumEnums: 0, 246 | NumMessages: 2, 247 | NumExtensions: 0, 248 | NumServices: 0, 249 | }, 250 | GoTypes: file_opcua_publish_proto_goTypes, 251 | DependencyIndexes: file_opcua_publish_proto_depIdxs, 252 | MessageInfos: file_opcua_publish_proto_msgTypes, 253 | }.Build() 254 | File_opcua_publish_proto = out.File 255 | file_opcua_publish_proto_rawDesc = nil 256 | file_opcua_publish_proto_goTypes = nil 257 | file_opcua_publish_proto_depIdxs = nil 258 | } 259 | -------------------------------------------------------------------------------- /gateway/gateway.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto/tls" 7 | "fmt" 8 | "log" 9 | "os" 10 | "path/filepath" 11 | "sync" 12 | "time" 13 | 14 | "github.com/awcullen/machinist/cloud" 15 | "github.com/awcullen/machinist/opcua" 16 | "github.com/dgraph-io/badger/v2" 17 | mqtt "github.com/eclipse/paho.mqtt.golang" 18 | "github.com/pkg/errors" 19 | ) 20 | 21 | const ( 22 | compactionInterval = 5 * time.Minute 23 | ) 24 | 25 | // Start initializes the Gateway and connects to the local broker for configuration details. 26 | func Start(config cloud.Config) (*Gateway, error) { 27 | log.Printf("[gateway] Gateway '%s' starting.\n", config.DeviceID) 28 | g := &Gateway{ 29 | workers: &sync.WaitGroup{}, 30 | projectID: config.ProjectID, 31 | region: config.Region, 32 | registryID: config.RegistryID, 33 | deviceID: config.DeviceID, 34 | privateKey: config.PrivateKey, 35 | algorithm: config.Algorithm, 36 | storePath: config.StorePath, 37 | deviceMap: make(map[string]cloud.Device, 16), 38 | configCh: make(chan *Config, 4), 39 | startTime: time.Now().UTC(), 40 | } 41 | g.ctx, g.cancel = context.WithCancel(context.Background()) 42 | 43 | // ensure storePath directory exists 44 | dbPath := filepath.Join(g.storePath, g.deviceID) 45 | if err := os.MkdirAll(dbPath, 0777); err != nil { 46 | return nil, err 47 | } 48 | // open the database 49 | db, err := badger.Open(badger.DefaultOptions(dbPath).WithTruncate(true).WithLogger(nil)) 50 | if err != nil { 51 | os.Remove(dbPath) 52 | if err := os.MkdirAll(dbPath, 0777); err != nil { 53 | return nil, err 54 | } 55 | db, err = badger.Open(badger.DefaultOptions(dbPath).WithTruncate(true).WithLogger(nil)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | } 60 | g.db = db 61 | 62 | // read gateway config from store, if available 63 | config2 := new(Config) 64 | err = g.db.View(func(txn *badger.Txn) error { 65 | item, err := txn.Get([]byte("config")) 66 | if err != nil { 67 | return err 68 | } 69 | return item.Value(func(val []byte) error { 70 | return unmarshalConfig(val, config2) 71 | }) 72 | }) 73 | if err == nil { 74 | g.name = config2.Name 75 | g.location = config2.Location 76 | g.configCh <- config2 77 | } 78 | 79 | g.configTopic = fmt.Sprintf("/devices/%s/config", config.DeviceID) 80 | g.commandsTopic = fmt.Sprintf("/devices/%s/commands/#", config.DeviceID) 81 | g.eventsTopic = fmt.Sprintf("/devices/%s/events", config.DeviceID) 82 | 83 | opts := mqtt.NewClientOptions() 84 | opts.AddBroker(cloud.MqttBrokerURL) 85 | opts.SetProtocolVersion(cloud.ProtocolVersion) 86 | opts.SetClientID(fmt.Sprintf("projects/%s/locations/%s/registries/%s/devices/%s", config.ProjectID, config.Region, config.RegistryID, config.DeviceID)) 87 | opts.SetTLSConfig(&tls.Config{InsecureSkipVerify: true, ClientAuth: tls.NoClientCert}) 88 | opts.SetCredentialsProvider(g.onProvideCredentials) 89 | opts.SetConnectRetry(true) 90 | opts.SetMaxReconnectInterval(30 * time.Second) 91 | opts.SetOnConnectHandler(func(client mqtt.Client) { 92 | client.Subscribe(g.configTopic, 1, g.onConfig) 93 | client.Subscribe(g.commandsTopic, 1, g.onCommand) 94 | }) 95 | g.broker = mqtt.NewClient(opts) 96 | g.broker.Connect() 97 | 98 | // start all service workers 99 | g.startWorkers(g.ctx, g.cancel, g.workers) 100 | return g, nil 101 | } 102 | 103 | // Gateway stores the connected devices. 104 | type Gateway struct { 105 | sync.Mutex 106 | ctx context.Context 107 | cancel context.CancelFunc 108 | workers *sync.WaitGroup 109 | name string 110 | location string 111 | projectID string 112 | region string 113 | registryID string 114 | deviceID string 115 | privateKey string 116 | algorithm string 117 | storePath string 118 | configTopic string 119 | commandsTopic string 120 | eventsTopic string 121 | db *badger.DB 122 | broker mqtt.Client 123 | deviceMap map[string]cloud.Device 124 | startTime time.Time 125 | configTime time.Time 126 | configCh chan *Config 127 | } 128 | 129 | // Name ... 130 | func (g *Gateway) Name() string { 131 | return g.name 132 | } 133 | 134 | // Location ... 135 | func (g *Gateway) Location() string { 136 | return g.location 137 | } 138 | 139 | // DeviceID ... 140 | func (g *Gateway) DeviceID() string { 141 | return g.deviceID 142 | } 143 | 144 | // ProjectID ... 145 | func (g *Gateway) ProjectID() string { 146 | return g.projectID 147 | } 148 | 149 | // Region ... 150 | func (g *Gateway) Region() string { 151 | return g.region 152 | } 153 | 154 | // RegistryID ... 155 | func (g *Gateway) RegistryID() string { 156 | return g.registryID 157 | } 158 | 159 | // PrivateKey ... 160 | func (g *Gateway) PrivateKey() string { 161 | return g.privateKey 162 | } 163 | 164 | // Algorithm ... 165 | func (g *Gateway) Algorithm() string { 166 | return g.algorithm 167 | } 168 | 169 | // StorePath ... 170 | func (g *Gateway) StorePath() string { 171 | return g.storePath 172 | } 173 | 174 | // Devices ... 175 | func (g *Gateway) Devices() map[string]cloud.Device { 176 | return g.deviceMap 177 | } 178 | 179 | // Stop disconnects from the local broker and stops each device of the gateway. 180 | func (g *Gateway) Stop() error { 181 | log.Printf("[gateway] Gateway '%s' stopping.\n", g.deviceID) 182 | g.cancel() 183 | g.broker.Disconnect(3000) 184 | g.workers.Wait() 185 | g.db.Close() 186 | return nil 187 | } 188 | 189 | // startServices starts all services. 190 | func (g *Gateway) startWorkers(ctx context.Context, cancel context.CancelFunc, workers *sync.WaitGroup) { 191 | // start northbound service 192 | g.startNorthboundWorker(ctx, cancel, workers) 193 | // start southbound service 194 | g.startSouthboundWorker(ctx, cancel, workers) 195 | // start db compaction task 196 | g.startGCWorker(ctx, cancel, workers) 197 | } 198 | 199 | // startNorthboundWorker starts a task to handle communication to Google Clout IoT Core. 200 | func (g *Gateway) startNorthboundWorker(ctx context.Context, cancel context.CancelFunc, workers *sync.WaitGroup) { 201 | workers.Add(1) 202 | go func() { 203 | defer cancel() 204 | defer workers.Done() 205 | // loop till gateway cancelled 206 | for { 207 | select { 208 | case <-ctx.Done(): 209 | return 210 | } 211 | } 212 | }() 213 | } 214 | 215 | // startSouthboundWorker starts a task to handle starting and stopping devices. 216 | func (g *Gateway) startSouthboundWorker(ctx context.Context, cancel context.CancelFunc, workers *sync.WaitGroup) { 217 | workers.Add(1) 218 | go func() { 219 | defer cancel() 220 | defer workers.Done() 221 | // loop till gateway cancelled 222 | for { 223 | select { 224 | case <-ctx.Done(): 225 | g.stopDevices() 226 | return 227 | case config := <-g.configCh: 228 | g.stopDevices() 229 | g.startDevices(config) 230 | } 231 | } 232 | }() 233 | } 234 | 235 | // stopDevices stops each device of the gateway. 236 | func (g *Gateway) stopDevices() { 237 | g.Lock() 238 | defer g.Unlock() 239 | for k, v := range g.deviceMap { 240 | if err := v.Stop(); err != nil { 241 | log.Println(errors.Wrapf(err, "[gateway] Error stopping device '%s'", k)) 242 | } 243 | } 244 | g.deviceMap = make(map[string]cloud.Device, 16) 245 | return 246 | } 247 | 248 | // startDevices starts each device of the gateway. 249 | func (g *Gateway) startDevices(config *Config) { 250 | g.Lock() 251 | defer g.Unlock() 252 | // map the config into devices 253 | for _, conf := range config.Devices { 254 | n := cloud.Config{ 255 | ProjectID: g.projectID, 256 | Region: g.region, 257 | RegistryID: g.registryID, 258 | DeviceID: conf.DeviceID, 259 | PrivateKey: conf.PrivateKey, 260 | Algorithm: conf.Algorithm, 261 | StorePath: g.storePath, 262 | } 263 | switch conf.Kind { 264 | case "opcua": 265 | // start opcua device 266 | d, err := opcua.Start(n) 267 | if err != nil { 268 | log.Println(errors.Wrapf(err, "[gateway] Error starting opcua device '%s'", n.DeviceID)) 269 | continue 270 | } 271 | g.deviceMap[n.DeviceID] = d 272 | default: 273 | log.Println(errors.Errorf("[gateway] Error starting device '%s': kind unknown", n.DeviceID)) 274 | } 275 | } 276 | g.configTime = time.Now().UTC() 277 | return 278 | } 279 | 280 | // onProvideCredentials returns a username and password that is valid for this device. 281 | func (g *Gateway) onProvideCredentials() (string, string) { 282 | // With Google Cloud IoT Core, the username field is ignored (but must not be empty), and the 283 | // password field is used to transmit a JWT to authorize the device 284 | now := time.Now() 285 | exp := now.Add(cloud.TokenDuration) 286 | jwt, err := cloud.CreateJWT(g.projectID, g.privateKey, g.algorithm, now, exp) 287 | if err != nil { 288 | log.Println(errors.Wrap(err, "[gateway] Error creating jwt token")) 289 | return "ignored", "" 290 | } 291 | return "ignored", jwt 292 | } 293 | 294 | // onConfig handles messages to the 'config' topic of the MQTT broker. 295 | func (g *Gateway) onConfig(client mqtt.Client, msg mqtt.Message) { 296 | g.Lock() 297 | defer g.Unlock() 298 | var isDuplicate bool 299 | err := g.db.View(func(txn *badger.Txn) error { 300 | item, err := txn.Get([]byte("config")) 301 | if err != nil { 302 | return err 303 | } 304 | return item.Value(func(val []byte) error { 305 | isDuplicate = bytes.Equal(val, msg.Payload()) 306 | return nil 307 | }) 308 | }) 309 | if isDuplicate { 310 | return 311 | } 312 | config2 := new(Config) 313 | err = unmarshalConfig(msg.Payload(), config2) 314 | if err != nil { 315 | log.Println(errors.Wrap(err, "[gateway] Error unmarshalling config")) 316 | return 317 | } 318 | // store for next start 319 | err = g.db.Update(func(txn *badger.Txn) error { 320 | return txn.Set([]byte("config"), msg.Payload()) 321 | }) 322 | if err != nil { 323 | log.Println(errors.Wrap(err, "[gateway] Error storing config")) 324 | } 325 | g.name = config2.Name 326 | g.location = config2.Location 327 | // restart devices 328 | g.configCh <- config2 329 | } 330 | 331 | func (g *Gateway) onCommand(client mqtt.Client, msg mqtt.Message) { 332 | switch { 333 | default: 334 | log.Printf("[gateway] Gateway '%s' received unknown command '%s':\n", g.deviceID, msg.Topic()) 335 | } 336 | } 337 | 338 | // startGCWorker starts a task to periodically compact the database, removing deleted values. 339 | func (g *Gateway) startGCWorker(ctx context.Context, cancel context.CancelFunc, workers *sync.WaitGroup) { 340 | workers.Add(1) 341 | go func() { 342 | defer cancel() 343 | defer workers.Done() 344 | ticker := time.NewTicker(compactionInterval) 345 | defer ticker.Stop() 346 | for { 347 | select { 348 | case <-ctx.Done(): 349 | return 350 | case <-ticker.C: 351 | for { 352 | err := g.db.RunValueLogGC(0.5) 353 | if err != nil { 354 | break 355 | } 356 | } 357 | } 358 | } 359 | }() 360 | } 361 | -------------------------------------------------------------------------------- /opcua/write.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.20.1 4 | // protoc v3.11.4 5 | // source: opcua/write.proto 6 | 7 | package opcua 8 | 9 | import ( 10 | context "context" 11 | proto "github.com/golang/protobuf/proto" 12 | grpc "google.golang.org/grpc" 13 | codes "google.golang.org/grpc/codes" 14 | status "google.golang.org/grpc/status" 15 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 16 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 17 | reflect "reflect" 18 | sync "sync" 19 | ) 20 | 21 | const ( 22 | // Verify that this generated code is sufficiently up-to-date. 23 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 24 | // Verify that runtime/protoimpl is sufficiently up-to-date. 25 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 26 | ) 27 | 28 | // This is a compile-time assertion that a sufficiently up-to-date version 29 | // of the legacy proto package is being used. 30 | const _ = proto.ProtoPackageIsVersion4 31 | 32 | // WriteValue message. 33 | type WriteValue struct { 34 | state protoimpl.MessageState 35 | sizeCache protoimpl.SizeCache 36 | unknownFields protoimpl.UnknownFields 37 | 38 | NodeId string `protobuf:"bytes,1,opt,name=nodeId,proto3" json:"nodeId,omitempty"` 39 | AttributeId uint32 `protobuf:"varint,2,opt,name=attributeId,proto3" json:"attributeId,omitempty"` 40 | IndexRange string `protobuf:"bytes,3,opt,name=indexRange,proto3" json:"indexRange,omitempty"` 41 | Value *Variant `protobuf:"bytes,4,opt,name=value,proto3" json:"value,omitempty"` 42 | } 43 | 44 | func (x *WriteValue) Reset() { 45 | *x = WriteValue{} 46 | if protoimpl.UnsafeEnabled { 47 | mi := &file_opcua_write_proto_msgTypes[0] 48 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 49 | ms.StoreMessageInfo(mi) 50 | } 51 | } 52 | 53 | func (x *WriteValue) String() string { 54 | return protoimpl.X.MessageStringOf(x) 55 | } 56 | 57 | func (*WriteValue) ProtoMessage() {} 58 | 59 | func (x *WriteValue) ProtoReflect() protoreflect.Message { 60 | mi := &file_opcua_write_proto_msgTypes[0] 61 | if protoimpl.UnsafeEnabled && x != nil { 62 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 63 | if ms.LoadMessageInfo() == nil { 64 | ms.StoreMessageInfo(mi) 65 | } 66 | return ms 67 | } 68 | return mi.MessageOf(x) 69 | } 70 | 71 | // Deprecated: Use WriteValue.ProtoReflect.Descriptor instead. 72 | func (*WriteValue) Descriptor() ([]byte, []int) { 73 | return file_opcua_write_proto_rawDescGZIP(), []int{0} 74 | } 75 | 76 | func (x *WriteValue) GetNodeId() string { 77 | if x != nil { 78 | return x.NodeId 79 | } 80 | return "" 81 | } 82 | 83 | func (x *WriteValue) GetAttributeId() uint32 { 84 | if x != nil { 85 | return x.AttributeId 86 | } 87 | return 0 88 | } 89 | 90 | func (x *WriteValue) GetIndexRange() string { 91 | if x != nil { 92 | return x.IndexRange 93 | } 94 | return "" 95 | } 96 | 97 | func (x *WriteValue) GetValue() *Variant { 98 | if x != nil { 99 | return x.Value 100 | } 101 | return nil 102 | } 103 | 104 | // WriteRequest message 105 | type WriteRequest struct { 106 | state protoimpl.MessageState 107 | sizeCache protoimpl.SizeCache 108 | unknownFields protoimpl.UnknownFields 109 | 110 | RequestHandle uint32 `protobuf:"varint,1,opt,name=requestHandle,proto3" json:"requestHandle,omitempty"` 111 | NodesToWrite []*WriteValue `protobuf:"bytes,2,rep,name=nodesToWrite,proto3" json:"nodesToWrite,omitempty"` 112 | } 113 | 114 | func (x *WriteRequest) Reset() { 115 | *x = WriteRequest{} 116 | if protoimpl.UnsafeEnabled { 117 | mi := &file_opcua_write_proto_msgTypes[1] 118 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 119 | ms.StoreMessageInfo(mi) 120 | } 121 | } 122 | 123 | func (x *WriteRequest) String() string { 124 | return protoimpl.X.MessageStringOf(x) 125 | } 126 | 127 | func (*WriteRequest) ProtoMessage() {} 128 | 129 | func (x *WriteRequest) ProtoReflect() protoreflect.Message { 130 | mi := &file_opcua_write_proto_msgTypes[1] 131 | if protoimpl.UnsafeEnabled && x != nil { 132 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 133 | if ms.LoadMessageInfo() == nil { 134 | ms.StoreMessageInfo(mi) 135 | } 136 | return ms 137 | } 138 | return mi.MessageOf(x) 139 | } 140 | 141 | // Deprecated: Use WriteRequest.ProtoReflect.Descriptor instead. 142 | func (*WriteRequest) Descriptor() ([]byte, []int) { 143 | return file_opcua_write_proto_rawDescGZIP(), []int{1} 144 | } 145 | 146 | func (x *WriteRequest) GetRequestHandle() uint32 { 147 | if x != nil { 148 | return x.RequestHandle 149 | } 150 | return 0 151 | } 152 | 153 | func (x *WriteRequest) GetNodesToWrite() []*WriteValue { 154 | if x != nil { 155 | return x.NodesToWrite 156 | } 157 | return nil 158 | } 159 | 160 | // WriteResponse message. 161 | type WriteResponse struct { 162 | state protoimpl.MessageState 163 | sizeCache protoimpl.SizeCache 164 | unknownFields protoimpl.UnknownFields 165 | 166 | RequestHandle uint32 `protobuf:"varint,1,opt,name=requestHandle,proto3" json:"requestHandle,omitempty"` 167 | StatusCode uint32 `protobuf:"varint,2,opt,name=statusCode,proto3" json:"statusCode,omitempty"` 168 | Results []uint32 `protobuf:"varint,3,rep,packed,name=results,proto3" json:"results,omitempty"` 169 | } 170 | 171 | func (x *WriteResponse) Reset() { 172 | *x = WriteResponse{} 173 | if protoimpl.UnsafeEnabled { 174 | mi := &file_opcua_write_proto_msgTypes[2] 175 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 176 | ms.StoreMessageInfo(mi) 177 | } 178 | } 179 | 180 | func (x *WriteResponse) String() string { 181 | return protoimpl.X.MessageStringOf(x) 182 | } 183 | 184 | func (*WriteResponse) ProtoMessage() {} 185 | 186 | func (x *WriteResponse) ProtoReflect() protoreflect.Message { 187 | mi := &file_opcua_write_proto_msgTypes[2] 188 | if protoimpl.UnsafeEnabled && x != nil { 189 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 190 | if ms.LoadMessageInfo() == nil { 191 | ms.StoreMessageInfo(mi) 192 | } 193 | return ms 194 | } 195 | return mi.MessageOf(x) 196 | } 197 | 198 | // Deprecated: Use WriteResponse.ProtoReflect.Descriptor instead. 199 | func (*WriteResponse) Descriptor() ([]byte, []int) { 200 | return file_opcua_write_proto_rawDescGZIP(), []int{2} 201 | } 202 | 203 | func (x *WriteResponse) GetRequestHandle() uint32 { 204 | if x != nil { 205 | return x.RequestHandle 206 | } 207 | return 0 208 | } 209 | 210 | func (x *WriteResponse) GetStatusCode() uint32 { 211 | if x != nil { 212 | return x.StatusCode 213 | } 214 | return 0 215 | } 216 | 217 | func (x *WriteResponse) GetResults() []uint32 { 218 | if x != nil { 219 | return x.Results 220 | } 221 | return nil 222 | } 223 | 224 | var File_opcua_write_proto protoreflect.FileDescriptor 225 | 226 | var file_opcua_write_proto_rawDesc = []byte{ 227 | 0x0a, 0x11, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x2f, 0x77, 0x72, 0x69, 0x74, 0x65, 0x2e, 0x70, 0x72, 228 | 0x6f, 0x74, 0x6f, 0x12, 0x05, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x1a, 0x13, 0x6f, 0x70, 0x63, 0x75, 229 | 0x61, 0x2f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 230 | 0x8c, 0x01, 0x0a, 0x0a, 0x57, 0x72, 0x69, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x16, 231 | 0x0a, 0x06, 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 232 | 0x6e, 0x6f, 0x64, 0x65, 0x49, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 233 | 0x75, 0x74, 0x65, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x61, 0x74, 0x74, 234 | 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x6e, 0x64, 0x65, 235 | 0x78, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 236 | 0x64, 0x65, 0x78, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x24, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 237 | 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x2e, 238 | 0x56, 0x61, 0x72, 0x69, 0x61, 0x6e, 0x74, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x6b, 239 | 0x0a, 0x0c, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 240 | 0x0a, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 241 | 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x61, 242 | 0x6e, 0x64, 0x6c, 0x65, 0x12, 0x35, 0x0a, 0x0c, 0x6e, 0x6f, 0x64, 0x65, 0x73, 0x54, 0x6f, 0x57, 243 | 0x72, 0x69, 0x74, 0x65, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x6f, 0x70, 0x63, 244 | 0x75, 0x61, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0c, 0x6e, 245 | 0x6f, 0x64, 0x65, 0x73, 0x54, 0x6f, 0x57, 0x72, 0x69, 0x74, 0x65, 0x22, 0x6f, 0x0a, 0x0d, 0x57, 246 | 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x0d, 247 | 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x18, 0x01, 0x20, 248 | 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x61, 0x6e, 0x64, 249 | 0x6c, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 0x64, 0x65, 250 | 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x43, 0x6f, 251 | 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x18, 0x03, 0x20, 252 | 0x03, 0x28, 0x0d, 0x52, 0x07, 0x72, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x73, 0x32, 0x44, 0x0a, 0x0c, 253 | 0x57, 0x72, 0x69, 0x74, 0x65, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x05, 254 | 0x57, 0x72, 0x69, 0x74, 0x65, 0x12, 0x13, 0x2e, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x2e, 0x57, 0x72, 255 | 0x69, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x6f, 0x70, 0x63, 256 | 0x75, 0x61, 0x2e, 0x57, 0x72, 0x69, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 257 | 0x22, 0x00, 0x42, 0x09, 0x5a, 0x07, 0x2e, 0x3b, 0x6f, 0x70, 0x63, 0x75, 0x61, 0x62, 0x06, 0x70, 258 | 0x72, 0x6f, 0x74, 0x6f, 0x33, 259 | } 260 | 261 | var ( 262 | file_opcua_write_proto_rawDescOnce sync.Once 263 | file_opcua_write_proto_rawDescData = file_opcua_write_proto_rawDesc 264 | ) 265 | 266 | func file_opcua_write_proto_rawDescGZIP() []byte { 267 | file_opcua_write_proto_rawDescOnce.Do(func() { 268 | file_opcua_write_proto_rawDescData = protoimpl.X.CompressGZIP(file_opcua_write_proto_rawDescData) 269 | }) 270 | return file_opcua_write_proto_rawDescData 271 | } 272 | 273 | var file_opcua_write_proto_msgTypes = make([]protoimpl.MessageInfo, 3) 274 | var file_opcua_write_proto_goTypes = []interface{}{ 275 | (*WriteValue)(nil), // 0: opcua.WriteValue 276 | (*WriteRequest)(nil), // 1: opcua.WriteRequest 277 | (*WriteResponse)(nil), // 2: opcua.WriteResponse 278 | (*Variant)(nil), // 3: opcua.Variant 279 | } 280 | var file_opcua_write_proto_depIdxs = []int32{ 281 | 3, // 0: opcua.WriteValue.value:type_name -> opcua.Variant 282 | 0, // 1: opcua.WriteRequest.nodesToWrite:type_name -> opcua.WriteValue 283 | 1, // 2: opcua.WriteService.Write:input_type -> opcua.WriteRequest 284 | 2, // 3: opcua.WriteService.Write:output_type -> opcua.WriteResponse 285 | 3, // [3:4] is the sub-list for method output_type 286 | 2, // [2:3] is the sub-list for method input_type 287 | 2, // [2:2] is the sub-list for extension type_name 288 | 2, // [2:2] is the sub-list for extension extendee 289 | 0, // [0:2] is the sub-list for field type_name 290 | } 291 | 292 | func init() { file_opcua_write_proto_init() } 293 | func file_opcua_write_proto_init() { 294 | if File_opcua_write_proto != nil { 295 | return 296 | } 297 | file_opcua_variant_proto_init() 298 | if !protoimpl.UnsafeEnabled { 299 | file_opcua_write_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 300 | switch v := v.(*WriteValue); i { 301 | case 0: 302 | return &v.state 303 | case 1: 304 | return &v.sizeCache 305 | case 2: 306 | return &v.unknownFields 307 | default: 308 | return nil 309 | } 310 | } 311 | file_opcua_write_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 312 | switch v := v.(*WriteRequest); i { 313 | case 0: 314 | return &v.state 315 | case 1: 316 | return &v.sizeCache 317 | case 2: 318 | return &v.unknownFields 319 | default: 320 | return nil 321 | } 322 | } 323 | file_opcua_write_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 324 | switch v := v.(*WriteResponse); i { 325 | case 0: 326 | return &v.state 327 | case 1: 328 | return &v.sizeCache 329 | case 2: 330 | return &v.unknownFields 331 | default: 332 | return nil 333 | } 334 | } 335 | } 336 | type x struct{} 337 | out := protoimpl.TypeBuilder{ 338 | File: protoimpl.DescBuilder{ 339 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 340 | RawDescriptor: file_opcua_write_proto_rawDesc, 341 | NumEnums: 0, 342 | NumMessages: 3, 343 | NumExtensions: 0, 344 | NumServices: 1, 345 | }, 346 | GoTypes: file_opcua_write_proto_goTypes, 347 | DependencyIndexes: file_opcua_write_proto_depIdxs, 348 | MessageInfos: file_opcua_write_proto_msgTypes, 349 | }.Build() 350 | File_opcua_write_proto = out.File 351 | file_opcua_write_proto_rawDesc = nil 352 | file_opcua_write_proto_goTypes = nil 353 | file_opcua_write_proto_depIdxs = nil 354 | } 355 | 356 | // Reference imports to suppress errors if they are not otherwise used. 357 | var _ context.Context 358 | var _ grpc.ClientConnInterface 359 | 360 | // This is a compile-time assertion to ensure that this generated file 361 | // is compatible with the grpc package it is being compiled against. 362 | const _ = grpc.SupportPackageIsVersion6 363 | 364 | // WriteServiceClient is the client API for WriteService service. 365 | // 366 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 367 | type WriteServiceClient interface { 368 | Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) 369 | } 370 | 371 | type writeServiceClient struct { 372 | cc grpc.ClientConnInterface 373 | } 374 | 375 | func NewWriteServiceClient(cc grpc.ClientConnInterface) WriteServiceClient { 376 | return &writeServiceClient{cc} 377 | } 378 | 379 | func (c *writeServiceClient) Write(ctx context.Context, in *WriteRequest, opts ...grpc.CallOption) (*WriteResponse, error) { 380 | out := new(WriteResponse) 381 | err := c.cc.Invoke(ctx, "/opcua.WriteService/Write", in, out, opts...) 382 | if err != nil { 383 | return nil, err 384 | } 385 | return out, nil 386 | } 387 | 388 | // WriteServiceServer is the server API for WriteService service. 389 | type WriteServiceServer interface { 390 | Write(context.Context, *WriteRequest) (*WriteResponse, error) 391 | } 392 | 393 | // UnimplementedWriteServiceServer can be embedded to have forward compatible implementations. 394 | type UnimplementedWriteServiceServer struct { 395 | } 396 | 397 | func (*UnimplementedWriteServiceServer) Write(context.Context, *WriteRequest) (*WriteResponse, error) { 398 | return nil, status.Errorf(codes.Unimplemented, "method Write not implemented") 399 | } 400 | 401 | func RegisterWriteServiceServer(s *grpc.Server, srv WriteServiceServer) { 402 | s.RegisterService(&_WriteService_serviceDesc, srv) 403 | } 404 | 405 | func _WriteService_Write_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 406 | in := new(WriteRequest) 407 | if err := dec(in); err != nil { 408 | return nil, err 409 | } 410 | if interceptor == nil { 411 | return srv.(WriteServiceServer).Write(ctx, in) 412 | } 413 | info := &grpc.UnaryServerInfo{ 414 | Server: srv, 415 | FullMethod: "/opcua.WriteService/Write", 416 | } 417 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 418 | return srv.(WriteServiceServer).Write(ctx, req.(*WriteRequest)) 419 | } 420 | return interceptor(ctx, in, info, handler) 421 | } 422 | 423 | var _WriteService_serviceDesc = grpc.ServiceDesc{ 424 | ServiceName: "opcua.WriteService", 425 | HandlerType: (*WriteServiceServer)(nil), 426 | Methods: []grpc.MethodDesc{ 427 | { 428 | MethodName: "Write", 429 | Handler: _WriteService_Write_Handler, 430 | }, 431 | }, 432 | Streams: []grpc.StreamDesc{}, 433 | Metadata: "opcua/write.proto", 434 | } 435 | -------------------------------------------------------------------------------- /spa/src/js/gauge.min.js: -------------------------------------------------------------------------------- 1 | (function(){var t,i,e,s,n,o,a,h,r,l,p,c,u,d=[].slice,g={}.hasOwnProperty,m=function(t,i){function e(){this.constructor=t}for(var s in i)g.call(i,s)&&(t[s]=i[s]);return e.prototype=i.prototype,t.prototype=new e,t.__super__=i.prototype,t};!function(){var t,i,e,s,n,o,a;for(a=["ms","moz","webkit","o"],e=0,n=a.length;e1&&(n="."+e[1]),i=/(\d+)(\d{3})/;i.test(s);)s=s.replace(i,"$1,$2");return s+n},l=function(t){return"#"===t.charAt(0)?t.substring(1,7):t},h=function(){function t(t,i){null==t&&(t=!0),this.clear=null==i||i,t&&AnimationUpdater.add(this)}return t.prototype.animationSpeed=32,t.prototype.update=function(t){var i;return null==t&&(t=!1),!(!t&&this.displayedValue===this.value)&&(this.ctx&&this.clear&&this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),i=this.value-this.displayedValue,Math.abs(i/this.animationSpeed)<=.001?this.displayedValue=this.value:this.displayedValue=this.displayedValue+i/this.animationSpeed,this.render(),!0)},t}(),e=function(t){function i(){return i.__super__.constructor.apply(this,arguments)}return m(i,t),i.prototype.displayScale=1,i.prototype.forceUpdate=!0,i.prototype.setTextField=function(t,i){return this.textField=t instanceof a?t:new a(t,i)},i.prototype.setMinValue=function(t,i){var e,s,n,o,a;if(this.minValue=t,null==i&&(i=!0),i){for(this.displayedValue=this.minValue,o=this.gp||[],a=[],s=0,n=o.length;s.5&&(this.options.angle=.5),this.configDisplayScale(),this},i.prototype.configDisplayScale=function(){var t,i,e,s,n;return s=this.displayScale,!1===this.options.highDpiSupport?delete this.displayScale:(i=window.devicePixelRatio||1,t=this.ctx.webkitBackingStorePixelRatio||this.ctx.mozBackingStorePixelRatio||this.ctx.msBackingStorePixelRatio||this.ctx.oBackingStorePixelRatio||this.ctx.backingStorePixelRatio||1,this.displayScale=i/t),this.displayScale!==s&&(n=this.canvas.G__width||this.canvas.width,e=this.canvas.G__height||this.canvas.height,this.canvas.width=n*this.displayScale,this.canvas.height=e*this.displayScale,this.canvas.style.width=n+"px",this.canvas.style.height=e+"px",this.canvas.G__width=n,this.canvas.G__height=e),this},i.prototype.parseValue=function(t){return t=parseFloat(t)||Number(t),isFinite(t)?t:0},i}(h),a=function(){function t(t,i){this.el=t,this.fractionDigits=i}return t.prototype.render=function(t){return this.el.innerHTML=p(t.displayedValue,this.fractionDigits)},t}(),t=function(t){function i(t,e){if(this.elem=t,this.text=null!=e&&e,i.__super__.constructor.call(this),void 0===this.elem)throw new Error("The element isn't defined.");this.value=1*this.elem.innerHTML,this.text&&(this.value=0)}return m(i,t),i.prototype.displayedValue=0,i.prototype.value=0,i.prototype.setVal=function(t){return this.value=1*t},i.prototype.render=function(){var t;return t=this.text?u(this.displayedValue.toFixed(0)):r(p(this.displayedValue)),this.elem.innerHTML=t},i}(h),o=function(t){function i(t){if(this.gauge=t,void 0===this.gauge)throw new Error("The element isn't defined.");this.ctx=this.gauge.ctx,this.canvas=this.gauge.canvas,i.__super__.constructor.call(this,!1,!1),this.setOptions()}return m(i,t),i.prototype.displayedValue=0,i.prototype.value=0,i.prototype.options={strokeWidth:.035,length:.1,color:"#000000",iconPath:null,iconScale:1,iconAngle:0},i.prototype.img=null,i.prototype.setOptions=function(t){if(null==t&&(t=null),this.options=c(this.options,t),this.length=2*this.gauge.radius*this.gauge.options.radiusScale*this.options.length,this.strokeWidth=this.canvas.height*this.options.strokeWidth,this.maxValue=this.gauge.maxValue,this.minValue=this.gauge.minValue,this.animationSpeed=this.gauge.animationSpeed,this.options.angle=this.gauge.options.angle,this.options.iconPath)return this.img=new Image,this.img.src=this.options.iconPath},i.prototype.render=function(){var t,i,e,s,n,o,a,h,r;if(t=this.gauge.getAngle.call(this,this.displayedValue),h=Math.round(this.length*Math.cos(t)),r=Math.round(this.length*Math.sin(t)),o=Math.round(this.strokeWidth*Math.cos(t-Math.PI/2)),a=Math.round(this.strokeWidth*Math.sin(t-Math.PI/2)),i=Math.round(this.strokeWidth*Math.cos(t+Math.PI/2)),e=Math.round(this.strokeWidth*Math.sin(t+Math.PI/2)),this.ctx.beginPath(),this.ctx.fillStyle=this.options.color,this.ctx.arc(0,0,this.strokeWidth,0,2*Math.PI,!1),this.ctx.fill(),this.ctx.beginPath(),this.ctx.moveTo(o,a),this.ctx.lineTo(h,r),this.ctx.lineTo(i,e),this.ctx.fill(),this.img)return s=Math.round(this.img.width*this.options.iconScale),n=Math.round(this.img.height*this.options.iconScale),this.ctx.save(),this.ctx.translate(h,r),this.ctx.rotate(t+Math.PI/180*(90+this.options.iconAngle)),this.ctx.drawImage(this.img,-s/2,-n/2,s,n),this.ctx.restore()},i}(h),function(){function t(t){this.elem=t}t.prototype.updateValues=function(t){return this.value=t[0],this.maxValue=t[1],this.avgValue=t[2],this.render()},t.prototype.render=function(){var t,i;return this.textField&&this.textField.text(p(this.value)),0===this.maxValue&&(this.maxValue=2*this.avgValue),i=this.value/this.maxValue*100,t=this.avgValue/this.maxValue*100,$(".bar-value",this.elem).css({width:i+"%"}),$(".typical-value",this.elem).css({width:t+"%"})}}(),n=function(t){function i(t){var e,s;this.canvas=t,i.__super__.constructor.call(this),this.percentColors=null,"undefined"!=typeof G_vmlCanvasManager&&(this.canvas=window.G_vmlCanvasManager.initElement(this.canvas)),this.ctx=this.canvas.getContext("2d"),e=this.canvas.clientHeight,s=this.canvas.clientWidth,this.canvas.height=e,this.canvas.width=s,this.gp=[new o(this)],this.setOptions()}return m(i,t),i.prototype.elem=null,i.prototype.value=[20],i.prototype.maxValue=80,i.prototype.minValue=0,i.prototype.displayedAngle=0,i.prototype.displayedValue=0,i.prototype.lineWidth=40,i.prototype.paddingTop=.1,i.prototype.paddingBottom=.1,i.prototype.percentColors=null,i.prototype.options={colorStart:"#6fadcf",colorStop:void 0,gradientType:0,strokeColor:"#e0e0e0",pointer:{length:.8,strokeWidth:.035,iconScale:1},angle:.15,lineWidth:.44,radiusScale:1,fontSize:40,limitMax:!1,limitMin:!1},i.prototype.setOptions=function(t){var e,s,n,o,a;for(null==t&&(t=null),i.__super__.setOptions.call(this,t),this.configPercentColors(),this.extraPadding=0,this.options.angle<0&&(o=Math.PI*(1+this.options.angle),this.extraPadding=Math.sin(o)),this.availableHeight=this.canvas.height*(1-this.paddingTop-this.paddingBottom),this.lineWidth=this.availableHeight*this.options.lineWidth,this.radius=(this.availableHeight-this.lineWidth/2)/(1+this.extraPadding),this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),a=this.gp,s=0,n=a.length;s=n;e=0<=n?++s:--s)a=parseInt(l(this.options.percentColors[e][1]).substring(0,2),16),i=parseInt(l(this.options.percentColors[e][1]).substring(2,4),16),t=parseInt(l(this.options.percentColors[e][1]).substring(4,6),16),o.push(this.percentColors[e]={pct:this.options.percentColors[e][0],color:{r:a,g:i,b:t}});return o}},i.prototype.set=function(t){var i,e,s,n,a,h,r,l,p;for(t instanceof Array||(t=[t]),e=s=0,r=t.length-1;0<=r?s<=r:s>=r;e=0<=r?++s:--s)t[e]=this.parseValue(t[e]);if(t.length>this.gp.length)for(e=n=0,l=t.length-this.gp.length;0<=l?nl;e=0<=l?++n:--n)i=new o(this),i.setOptions(this.options.pointer),this.gp.push(i);else t.lengththis.maxValue?this.options.limitMax?p=this.maxValue:this.maxValue=p+1:p=h;n=0<=h?++o:--o)if(t<=this.percentColors[n].pct){!0===i?(r=this.percentColors[n-1]||this.percentColors[0],s=this.percentColors[n],a=(t-r.pct)/(s.pct-r.pct),e={r:Math.floor(r.color.r*(1-a)+s.color.r*a),g:Math.floor(r.color.g*(1-a)+s.color.g*a),b:Math.floor(r.color.b*(1-a)+s.color.b*a)}):e=this.percentColors[n].color;break}return"rgb("+[e.r,e.g,e.b].join(",")+")"},i.prototype.getColorForValue=function(t,i){var e;return e=(t-this.minValue)/(this.maxValue-this.minValue),this.getColorForPercentage(e,i)},i.prototype.renderStaticLabels=function(t,i,e,s){var n,o,a,h,r,l,c,u,d,g;for(this.ctx.save(),this.ctx.translate(i,e),n=t.font||"10px Times",l=/\d+\.?\d?/,r=n.match(l)[0],u=n.slice(r.length),o=parseFloat(r)*this.displayScale,this.ctx.font=o+u,this.ctx.fillStyle=t.color||"#000000",this.ctx.textBaseline="bottom",this.ctx.textAlign="center",c=t.labels,a=0,h=c.length;a=this.minValue)&&(!this.options.limitMax||g<=this.maxValue)&&(n=g.font||t.font,r=n.match(l)[0],u=n.slice(r.length),o=parseFloat(r)*this.displayScale,this.ctx.font=o+u,d=this.getAngle(g.label)-3*Math.PI/2,this.ctx.rotate(d),this.ctx.fillText(p(g.label,t.fractionDigits),0,-s-this.lineWidth/2),this.ctx.rotate(-d)):(!this.options.limitMin||g>=this.minValue)&&(!this.options.limitMax||g<=this.maxValue)&&(d=this.getAngle(g)-3*Math.PI/2,this.ctx.rotate(d),this.ctx.fillText(p(g,t.fractionDigits),0,-s-this.lineWidth/2),this.ctx.rotate(-d));return this.ctx.restore()},i.prototype.renderTicks=function(t,i,e,s){var n,o,a,h,r,l,p,c,u,d,g,m,x,f,v,y,V,w,S,M,C;if(t!=={}){for(l=t.divisions||0,S=t.subDivisions||0,a=t.divColor||"#fff",v=t.subColor||"#fff",h=t.divLength||.7,V=t.subLength||.2,u=parseFloat(this.maxValue)-parseFloat(this.minValue),d=parseFloat(u)/parseFloat(t.divisions),y=parseFloat(d)/parseFloat(t.subDivisions),n=parseFloat(this.minValue),o=0+y,c=u/400,r=c*(t.divWidth||1),w=c*(t.subWidth||1),m=[],M=p=0,g=l+1;p0?m.push(function(){var t,i,e;for(e=[],f=t=0,i=S-1;tthis.maxValue&&(r=this.maxValue),g=this.radius*this.options.radiusScale,x.height&&(this.ctx.lineWidth=this.lineWidth*x.height,d=this.lineWidth/2*(x.offset||1-x.height),g=this.radius*this.options.radiusScale+d),this.ctx.strokeStyle=x.strokeStyle,this.ctx.beginPath(),this.ctx.arc(0,0,g,this.getAngle(l),this.getAngle(r),!1),this.ctx.stroke();else void 0!==this.options.customFillStyle?i=this.options.customFillStyle(this):null!==this.percentColors?i=this.getColorForValue(this.displayedValue,this.options.generateGradient):void 0!==this.options.colorStop?(i=0===this.options.gradientType?this.ctx.createRadialGradient(m,s,9,m,s,70):this.ctx.createLinearGradient(0,0,m,0),i.addColorStop(0,this.options.colorStart),i.addColorStop(1,this.options.colorStop)):i=this.options.colorStart,this.ctx.strokeStyle=i,this.ctx.beginPath(),this.ctx.arc(m,s,p,(1+this.options.angle)*Math.PI,t,!1),this.ctx.lineWidth=this.lineWidth,this.ctx.stroke(),this.ctx.strokeStyle=this.options.strokeColor,this.ctx.beginPath(),this.ctx.arc(m,s,p,t,(2-this.options.angle)*Math.PI,!1),this.ctx.stroke(),this.ctx.save(),this.ctx.translate(m,s);for(this.options.renderTicks&&this.renderTicks(this.options.renderTicks,m,s,p),this.ctx.restore(),this.ctx.translate(m,s),u=this.gp,o=0,h=u.length;othis.maxValue?this.options.limitMax?this.value=this.maxValue:this.maxValue=this.value:this.value opcua.BrowseDirectionEnum 877 | 3, // 1: opcua.BrowseRequest.nodesToBrowse:type_name -> opcua.BrowseDescription 878 | 5, // 2: opcua.BrowseResult.references:type_name -> opcua.ReferenceDescription 879 | 6, // 3: opcua.BrowseResponse.results:type_name -> opcua.BrowseResult 880 | 6, // 4: opcua.BrowseNextResponse.results:type_name -> opcua.BrowseResult 881 | 4, // 5: opcua.BrowseService.Browse:input_type -> opcua.BrowseRequest 882 | 8, // 6: opcua.BrowseService.BrowseNext:input_type -> opcua.BrowseNextRequest 883 | 7, // 7: opcua.BrowseService.Browse:output_type -> opcua.BrowseResponse 884 | 9, // 8: opcua.BrowseService.BrowseNext:output_type -> opcua.BrowseNextResponse 885 | 7, // [7:9] is the sub-list for method output_type 886 | 5, // [5:7] is the sub-list for method input_type 887 | 5, // [5:5] is the sub-list for extension type_name 888 | 5, // [5:5] is the sub-list for extension extendee 889 | 0, // [0:5] is the sub-list for field type_name 890 | } 891 | 892 | func init() { file_opcua_browse_proto_init() } 893 | func file_opcua_browse_proto_init() { 894 | if File_opcua_browse_proto != nil { 895 | return 896 | } 897 | if !protoimpl.UnsafeEnabled { 898 | file_opcua_browse_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 899 | switch v := v.(*BrowseDescription); i { 900 | case 0: 901 | return &v.state 902 | case 1: 903 | return &v.sizeCache 904 | case 2: 905 | return &v.unknownFields 906 | default: 907 | return nil 908 | } 909 | } 910 | file_opcua_browse_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 911 | switch v := v.(*BrowseRequest); i { 912 | case 0: 913 | return &v.state 914 | case 1: 915 | return &v.sizeCache 916 | case 2: 917 | return &v.unknownFields 918 | default: 919 | return nil 920 | } 921 | } 922 | file_opcua_browse_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { 923 | switch v := v.(*ReferenceDescription); i { 924 | case 0: 925 | return &v.state 926 | case 1: 927 | return &v.sizeCache 928 | case 2: 929 | return &v.unknownFields 930 | default: 931 | return nil 932 | } 933 | } 934 | file_opcua_browse_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { 935 | switch v := v.(*BrowseResult); i { 936 | case 0: 937 | return &v.state 938 | case 1: 939 | return &v.sizeCache 940 | case 2: 941 | return &v.unknownFields 942 | default: 943 | return nil 944 | } 945 | } 946 | file_opcua_browse_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { 947 | switch v := v.(*BrowseResponse); i { 948 | case 0: 949 | return &v.state 950 | case 1: 951 | return &v.sizeCache 952 | case 2: 953 | return &v.unknownFields 954 | default: 955 | return nil 956 | } 957 | } 958 | file_opcua_browse_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { 959 | switch v := v.(*BrowseNextRequest); i { 960 | case 0: 961 | return &v.state 962 | case 1: 963 | return &v.sizeCache 964 | case 2: 965 | return &v.unknownFields 966 | default: 967 | return nil 968 | } 969 | } 970 | file_opcua_browse_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { 971 | switch v := v.(*BrowseNextResponse); i { 972 | case 0: 973 | return &v.state 974 | case 1: 975 | return &v.sizeCache 976 | case 2: 977 | return &v.unknownFields 978 | default: 979 | return nil 980 | } 981 | } 982 | } 983 | type x struct{} 984 | out := protoimpl.TypeBuilder{ 985 | File: protoimpl.DescBuilder{ 986 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 987 | RawDescriptor: file_opcua_browse_proto_rawDesc, 988 | NumEnums: 3, 989 | NumMessages: 7, 990 | NumExtensions: 0, 991 | NumServices: 1, 992 | }, 993 | GoTypes: file_opcua_browse_proto_goTypes, 994 | DependencyIndexes: file_opcua_browse_proto_depIdxs, 995 | EnumInfos: file_opcua_browse_proto_enumTypes, 996 | MessageInfos: file_opcua_browse_proto_msgTypes, 997 | }.Build() 998 | File_opcua_browse_proto = out.File 999 | file_opcua_browse_proto_rawDesc = nil 1000 | file_opcua_browse_proto_goTypes = nil 1001 | file_opcua_browse_proto_depIdxs = nil 1002 | } 1003 | 1004 | // Reference imports to suppress errors if they are not otherwise used. 1005 | var _ context.Context 1006 | var _ grpc.ClientConnInterface 1007 | 1008 | // This is a compile-time assertion to ensure that this generated file 1009 | // is compatible with the grpc package it is being compiled against. 1010 | const _ = grpc.SupportPackageIsVersion6 1011 | 1012 | // BrowseServiceClient is the client API for BrowseService service. 1013 | // 1014 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 1015 | type BrowseServiceClient interface { 1016 | Browse(ctx context.Context, in *BrowseRequest, opts ...grpc.CallOption) (*BrowseResponse, error) 1017 | BrowseNext(ctx context.Context, in *BrowseNextRequest, opts ...grpc.CallOption) (*BrowseNextResponse, error) 1018 | } 1019 | 1020 | type browseServiceClient struct { 1021 | cc grpc.ClientConnInterface 1022 | } 1023 | 1024 | func NewBrowseServiceClient(cc grpc.ClientConnInterface) BrowseServiceClient { 1025 | return &browseServiceClient{cc} 1026 | } 1027 | 1028 | func (c *browseServiceClient) Browse(ctx context.Context, in *BrowseRequest, opts ...grpc.CallOption) (*BrowseResponse, error) { 1029 | out := new(BrowseResponse) 1030 | err := c.cc.Invoke(ctx, "/opcua.BrowseService/Browse", in, out, opts...) 1031 | if err != nil { 1032 | return nil, err 1033 | } 1034 | return out, nil 1035 | } 1036 | 1037 | func (c *browseServiceClient) BrowseNext(ctx context.Context, in *BrowseNextRequest, opts ...grpc.CallOption) (*BrowseNextResponse, error) { 1038 | out := new(BrowseNextResponse) 1039 | err := c.cc.Invoke(ctx, "/opcua.BrowseService/BrowseNext", in, out, opts...) 1040 | if err != nil { 1041 | return nil, err 1042 | } 1043 | return out, nil 1044 | } 1045 | 1046 | // BrowseServiceServer is the server API for BrowseService service. 1047 | type BrowseServiceServer interface { 1048 | Browse(context.Context, *BrowseRequest) (*BrowseResponse, error) 1049 | BrowseNext(context.Context, *BrowseNextRequest) (*BrowseNextResponse, error) 1050 | } 1051 | 1052 | // UnimplementedBrowseServiceServer can be embedded to have forward compatible implementations. 1053 | type UnimplementedBrowseServiceServer struct { 1054 | } 1055 | 1056 | func (*UnimplementedBrowseServiceServer) Browse(context.Context, *BrowseRequest) (*BrowseResponse, error) { 1057 | return nil, status.Errorf(codes.Unimplemented, "method Browse not implemented") 1058 | } 1059 | func (*UnimplementedBrowseServiceServer) BrowseNext(context.Context, *BrowseNextRequest) (*BrowseNextResponse, error) { 1060 | return nil, status.Errorf(codes.Unimplemented, "method BrowseNext not implemented") 1061 | } 1062 | 1063 | func RegisterBrowseServiceServer(s *grpc.Server, srv BrowseServiceServer) { 1064 | s.RegisterService(&_BrowseService_serviceDesc, srv) 1065 | } 1066 | 1067 | func _BrowseService_Browse_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 1068 | in := new(BrowseRequest) 1069 | if err := dec(in); err != nil { 1070 | return nil, err 1071 | } 1072 | if interceptor == nil { 1073 | return srv.(BrowseServiceServer).Browse(ctx, in) 1074 | } 1075 | info := &grpc.UnaryServerInfo{ 1076 | Server: srv, 1077 | FullMethod: "/opcua.BrowseService/Browse", 1078 | } 1079 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 1080 | return srv.(BrowseServiceServer).Browse(ctx, req.(*BrowseRequest)) 1081 | } 1082 | return interceptor(ctx, in, info, handler) 1083 | } 1084 | 1085 | func _BrowseService_BrowseNext_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 1086 | in := new(BrowseNextRequest) 1087 | if err := dec(in); err != nil { 1088 | return nil, err 1089 | } 1090 | if interceptor == nil { 1091 | return srv.(BrowseServiceServer).BrowseNext(ctx, in) 1092 | } 1093 | info := &grpc.UnaryServerInfo{ 1094 | Server: srv, 1095 | FullMethod: "/opcua.BrowseService/BrowseNext", 1096 | } 1097 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 1098 | return srv.(BrowseServiceServer).BrowseNext(ctx, req.(*BrowseNextRequest)) 1099 | } 1100 | return interceptor(ctx, in, info, handler) 1101 | } 1102 | 1103 | var _BrowseService_serviceDesc = grpc.ServiceDesc{ 1104 | ServiceName: "opcua.BrowseService", 1105 | HandlerType: (*BrowseServiceServer)(nil), 1106 | Methods: []grpc.MethodDesc{ 1107 | { 1108 | MethodName: "Browse", 1109 | Handler: _BrowseService_Browse_Handler, 1110 | }, 1111 | { 1112 | MethodName: "BrowseNext", 1113 | Handler: _BrowseService_BrowseNext_Handler, 1114 | }, 1115 | }, 1116 | Streams: []grpc.StreamDesc{}, 1117 | Metadata: "opcua/browse.proto", 1118 | } 1119 | --------------------------------------------------------------------------------