├── 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 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 | 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 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | Home
11 | Sys Info
12 | Monitor
13 |
14 | About
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
49 |
--------------------------------------------------------------------------------
/spa/src/components/Spinner.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
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 |
2 |
3 |
4 |
{{ title }}: {{ valComputed }}%
5 |
6 |
7 |
8 |
86 |
87 |
101 |
--------------------------------------------------------------------------------
/spa/src/components/Monitor.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | | {{ key | titleify }} |
13 | {{ val }} |
14 |
15 |
16 |
17 |
18 |
19 |
Environment Variables
20 |
{{envVars}}
21 |
22 |
23 |
24 |
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 |
2 |
3 |
6 |
7 |
8 |
9 |
Gateway
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | | Name |
18 | {{ info.name }} |
19 |
20 |
21 | | Location |
22 | {{ info.location }} |
23 |
24 |
25 | | Project Id |
26 | {{ info.projectId }} |
27 |
28 |
29 | | Region |
30 | {{ info.region }} |
31 |
32 |
33 | | Registry Id |
34 | {{ info.registryId }} |
35 |
36 |
37 | | Device Id |
38 | {{ info.deviceId }} |
39 |
40 |
41 | | Store Path |
42 | {{ info.storePath }} |
43 |
44 |
45 |
46 |
Machines
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | | Name |
55 | {{ device.name }} |
56 |
57 |
58 | | Location |
59 | {{ device.location }} |
60 |
61 |
62 | | Device Id |
63 | {{ device.deviceId }} |
64 |
65 |
66 | | Kind |
67 | {{ device.kind }} |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
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 |
--------------------------------------------------------------------------------