Skip to content

opcua module #310

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Apr 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/ci-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ jobs:
include:
- { platform: macos-14, arch: arm64, os: macos }
- { platform: macos-13, arch: x64, os: macos }
- { platform: ubuntu-20.04, arch: x64, os: linux }
- { platform: ubuntu-20.04, arch: arm32, os: linux }
- { platform: ubuntu-22.04, arch: x64, os: linux }
- { platform: ubuntu-22.04-arm, arch: arm64, os: linux }
- { platform: ubuntu-22.04, arch: arm32, os: linux }
- { platform: windows-2019, arch: x64, os: windows }
runs-on: ${{ matrix.platform }}
steps:
Expand Down
8 changes: 8 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"Debugf",
"Descs",
"dlemstra",
"EPSG",
"eventbus",
"Fetchable",
"Geomap",
Expand Down Expand Up @@ -95,6 +96,7 @@
"nrow",
"nrows",
"nums",
"opcua",
"orcaman",
"packagex",
"pkgs",
Expand Down Expand Up @@ -145,12 +147,18 @@
"ignoreWords": [
"APND",
"MGTPE",
"Peucker",
"akimaspline",
"chartcompat",
"deinit",
"fritschbutland",
"goja",
"gonum",
"gopcua",
"interp",
"linearregression",
"opensimplex",
"paulmach",
"piecewiseconstant",
"piecewiselinear"
],
Expand Down
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ require (
github.com/go-sql-driver/mysql v1.7.1
github.com/gofrs/uuid/v5 v5.3.0
github.com/golang-jwt/jwt/v4 v4.5.2
github.com/gopcua/opcua v0.7.4
github.com/gorilla/websocket v1.5.3
github.com/hashicorp/hcl/v2 v2.17.0
github.com/influxdata/line-protocol/v2 v2.2.1
Expand Down Expand Up @@ -111,6 +112,7 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/flatbuffers v1.12.1 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/json-iterator/go v1.1.12 // indirect
Expand Down Expand Up @@ -149,12 +151,12 @@ require (
go.opencensus.io v0.24.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
golang.org/x/arch v0.14.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d // indirect
golang.org/x/image v0.18.0 // indirect
golang.org/x/mod v0.19.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.36.0 // indirect
golang.org/x/sync v0.13.0 // indirect
golang.org/x/tools v0.23.0 // indirect
golang.org/x/tools v0.28.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
gonum.org/v1/plot v0.12.0 // indirect
google.golang.org/genproto v0.0.0-20231016165738-49dd2c1f3d0b // indirect
Expand Down
16 changes: 10 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,10 @@ github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopcua/opcua v0.7.4 h1:vVp+hatoV7bpU+hkLlBOLUGpYKEYSTS1U9t9J6/tC+Y=
github.com/gopcua/opcua v0.7.4/go.mod h1:Z6aellk0gIzznZd2UX+Syd/hUMBt65gRlTakpGo6se8=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
Expand Down Expand Up @@ -470,8 +474,8 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs=
golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d h1:0olWaB5pg3+oychR51GUVCEsGkeCU/2JxjBgIo4f3M0=
golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ=
golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
Expand All @@ -481,8 +485,8 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
Expand Down Expand Up @@ -571,8 +575,8 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
7 changes: 5 additions & 2 deletions mods/tql/fm_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package tql

import (
"bytes"
"context"
"encoding/csv"
"encoding/json"
"errors"
Expand Down Expand Up @@ -222,6 +223,7 @@ func (node *Node) fmScriptJS(initCode string, mainCode string, deinitCode string
}

type JSContext struct {
context.Context
vm *js.Runtime
sc *js.Program
node *Node
Expand Down Expand Up @@ -273,8 +275,9 @@ func jsSourceLoad(path string) ([]byte, error) {

func newJSContext(node *Node, initCode string, mainCode string, deinitCode string) (*JSContext, error) {
ctx := &JSContext{
node: node,
vm: js.New(),
Context: node.task.ctx,
node: node,
vm: js.New(),
}
ctx.vm.SetFieldNameMapper(js.TagFieldNameMapper("json", false))

Expand Down
137 changes: 134 additions & 3 deletions mods/tql/fm_script_modules.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"encoding/json"
"errors"
"fmt"
"io"
"math"
"math/rand"
"runtime"
Expand All @@ -15,6 +16,8 @@
js "github.com/dop251/goja"
"github.com/dop251/goja_nodejs/require"
"github.com/gofrs/uuid/v5"
"github.com/gopcua/opcua"
"github.com/gopcua/opcua/ua"
"github.com/machbase/neo-server/v8/api"
"github.com/machbase/neo-server/v8/mods/bridge"
"github.com/machbase/neo-server/v8/mods/nums"
Expand All @@ -36,6 +39,7 @@
registry.RegisterNativeModule("filter", ctx.nativeModuleFilter)
registry.RegisterNativeModule("analysis", ctx.nativeModuleAnalysis)
registry.RegisterNativeModule("spatial", ctx.nativeModuleSpatial)
registry.RegisterNativeModule("opcua", ctx.nativeModuleOpcua)
registry.Enable(ctx.vm)
}

Expand Down Expand Up @@ -310,7 +314,7 @@
})
return ret
})
// kalman = m.kalman(initalVariance, processVariance, ObservationVariance);
// kalman = m.kalman(initialVariance, processVariance, ObservationVariance);
// newValue = kalman.eval(time, ...vector);
o.Set("kalman", func(iv, pv, ov float64) js.Value {
var kf *kalman.KalmanFilter
Expand Down Expand Up @@ -569,14 +573,14 @@
// m.haversine(lat1, lon1, lat2, lon2)
// m.haversine([lat1, lon1], [lat2, lon2])
// m.haversine({radius: 1000, coordinates: [[lat1, lon1], [lat2, lon2]]})
o.Set("haversine", ctx.saptial_haversine)
o.Set("haversine", ctx.spatial_haversine)
// m.parseGeoJSON(value)
o.Set("parseGeoJSON", ctx.spatial_parseGeoJSON)
// m.simplify(tolerance, [lat1, lon1], [lat2, lon2], ...)
o.Set("simplify", ctx.spatial_simplify)
}

func (ctx *JSContext) saptial_haversine(call js.FunctionCall) js.Value {
func (ctx *JSContext) spatial_haversine(call js.FunctionCall) js.Value {
// EarthRadius is the radius of the earth in meters.
// To keep things consistent, this value matches WGS84 Web Mercator (EPSG:3867).
EarthRadius := 6378137.0 // meters
Expand Down Expand Up @@ -700,3 +704,130 @@
var _ = geoObj
return obj
}

func (ctx *JSContext) nativeModuleOpcua(r *js.Runtime, module *js.Object) {
// m = require("opcua")
o := module.Get("exports").(*js.Object)
// m.client({})
o.Set("client", ctx.opcua_client)

// MessageSecurityMode
o.Set("MessageSecurityModeNone", ua.MessageSecurityModeNone)
o.Set("MessageSecurityModeSign", ua.MessageSecurityModeSign)
o.Set("MessageSecurityModeSignAndEncrypt", ua.MessageSecurityModeSignAndEncrypt)

// TimestampsToReturn
o.Set("TimestampsToReturnSource", ua.TimestampsToReturnSource)
o.Set("TimestampsToReturnServer", ua.TimestampsToReturnServer)
o.Set("TimestampsToReturnBoth", ua.TimestampsToReturnBoth)
o.Set("TimestampsToReturnNeither", ua.TimestampsToReturnNeither)
o.Set("TimestampsToReturnInvalid", ua.TimestampsToReturnInvalid)
}

func (ctx *JSContext) opcua_client(call js.FunctionCall) js.Value {
opts := struct {
Endpoint string `json:"endpoint"`
MessageSecurityMode ua.MessageSecurityMode `json:"messageSecurityMode"`
}{
Endpoint: "opc.tcp://localhost:4840",
MessageSecurityMode: ua.MessageSecurityModeNone,
}
if len(call.Arguments) > 0 {
if err := ctx.vm.ExportTo(call.Arguments[0], &opts); err != nil {
return ctx.vm.NewGoError(fmt.Errorf("opcua.client: %s", err.Error()))
}

Check warning on line 738 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L737-L738

Added lines #L737 - L738 were not covered by tests
}

ret := ctx.vm.NewObject()
ret.Set("read", func(call js.FunctionCall) js.Value {
if len(call.Arguments) != 1 {
return ctx.vm.NewGoError(fmt.Errorf("opcua.read: missing argument"))
}

Check warning on line 745 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L744-L745

Added lines #L744 - L745 were not covered by tests
arg := struct {
MaxAge float64 `json:"maxAge"`
Nodes []string `json:"nodes"`
TimestampsToReturn ua.TimestampsToReturn `json:"timestampsToReturn"`
}{}
if err := ctx.vm.ExportTo(call.Arguments[0], &arg); err != nil {
return ctx.vm.NewGoError(fmt.Errorf("opcua.read: %s", err.Error()))
}

Check warning on line 753 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L752-L753

Added lines #L752 - L753 were not covered by tests
if len(arg.Nodes) == 0 {
return ctx.vm.NewGoError(fmt.Errorf("opcua.read: missing nodes"))
}

Check warning on line 756 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L755-L756

Added lines #L755 - L756 were not covered by tests

var rsp *ua.ReadResponse
var req = &ua.ReadRequest{
MaxAge: arg.MaxAge,
TimestampsToReturn: arg.TimestampsToReturn,
}
for _, n := range arg.Nodes {
id, err := ua.ParseNodeID(n)
if err != nil {
return ctx.vm.NewGoError(fmt.Errorf("opcua.read: %s", err.Error()))
}

Check warning on line 767 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L766-L767

Added lines #L766 - L767 were not covered by tests
req.NodesToRead = append(req.NodesToRead, &ua.ReadValueID{NodeID: id})
}

// TODO: how to keep the connection open?
// Create a new client
c, err := opcua.NewClient(opts.Endpoint, opcua.SecurityMode(opts.MessageSecurityMode))
if err != nil {
return ctx.vm.NewGoError(fmt.Errorf("opcua.client: %s", err.Error()))
}

Check warning on line 776 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L775-L776

Added lines #L775 - L776 were not covered by tests

if err := c.Connect(ctx); err != nil {
return ctx.vm.NewGoError(fmt.Errorf("opcua.client: %s", err.Error()))
}

Check warning on line 780 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L779-L780

Added lines #L779 - L780 were not covered by tests
defer c.Close(ctx)
// Close the connection when the function returns

for {
rsp, err = c.Read(ctx, req)
if err == nil {
break
}
switch {
case err == io.EOF && c.State() != opcua.Closed:
// has to be retried unless user closed the connection
time.After(1 * time.Second)
continue
case errors.Is(err, ua.StatusBadSessionIDInvalid):
// Session is not activated has to be retried. Session will be recreated internally.
time.After(1 * time.Second)
continue
case errors.Is(err, ua.StatusBadSessionNotActivated):
// Session is invalid has to be retried. Session will be recreated internally.
time.After(1 * time.Second)
continue
case errors.Is(err, ua.StatusBadSecureChannelIDInvalid):
// secure channel will be recreated internally.
time.After(1 * time.Second)
continue
default:
return ctx.vm.NewGoError(fmt.Errorf("opcua.read: %s", err.Error()))

Check warning on line 807 in mods/tql/fm_script_modules.go

View check run for this annotation

Codecov / codecov/patch

mods/tql/fm_script_modules.go#L789-L807

Added lines #L789 - L807 were not covered by tests
}
}
ret := []js.Value{}
for _, data := range rsp.Results {
code := ""
if c, ok := ua.StatusCodes[data.Status]; ok {
code = c.Name
}
ent := map[string]any{
"status": uint32(data.Status),
"statusText": data.Status.Error(),
"statusCode": code,
"sourceTimestamp": data.SourceTimestamp,
"serverTimestamp": data.ServerTimestamp,
"sourcePicoseconds": data.SourcePicoseconds,
"serverPicoseconds": data.ServerPicoseconds,
}
if data.Value != nil {
ent["value"] = data.Value.Value()
}
ret = append(ret, ctx.vm.ToValue(ent))
}
return ctx.vm.ToValue(ret)
})
return ret
}
Loading
Loading