diff --git a/.github/workflows/check_license.yml b/.github/workflows/check_license.yml index 92a7d9f..b4414c5 100755 --- a/.github/workflows/check_license.yml +++ b/.github/workflows/check_license.yml @@ -4,8 +4,8 @@ on: pull_request concurrency: - group: ${{ github.ref }}-${{ github.workflow }} - cancel-in-progress: true + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true jobs: check-spdx-headers: @@ -13,22 +13,21 @@ jobs: steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - # required to grab the history of the PR - fetch-depth: 0 - submodules: 'true' - - - name: Get changed files - run: | - echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | tr '\n' ',')" >> $GITHUB_ENV - - - name: Set license - run: | - echo "licenses=Apache-2.0" >> $GITHUB_ENV - - - uses: eclipse-kuksa/kuksa-actions/spdx@2 - with: - files: "${{ env.files }}" - licenses: "${{ env.licenses }}" + - name: Checkout code + uses: actions/checkout@v4 + with: + # required to grab the history of the PR + fetch-depth: 0 + + - name: Get changed files + run: | + echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | tr '\n' ',')" >> $GITHUB_ENV + + - name: Set license + run: | + echo "licenses=Apache-2.0" >> $GITHUB_ENV + + - uses: eclipse-kuksa/kuksa-actions/spdx@2 + with: + files: "${{ env.files }}" + licenses: "${{ env.licenses }}" diff --git a/.github/workflows/kuksa_go_client.yaml b/.github/workflows/kuksa_go_client.yaml new file mode 100644 index 0000000..3bdd4b0 --- /dev/null +++ b/.github/workflows/kuksa_go_client.yaml @@ -0,0 +1,59 @@ +# /******************************************************************************** +# * Copyright (c) 2022 Contributors to the Eclipse Foundation +# * +# * See the NOTICE file(s) distributed with this work for additional +# * information regarding copyright ownership. +# * +# * This program and the accompanying materials are made available under the +# * terms of the Apache License 2.0 which is available at +# * http://www.apache.org/licenses/LICENSE-2.0 +# * +# * SPDX-License-Identifier: Apache-2.0 +# ********************************************************************************/ + +name: kuksa_go_client + +on: + push: + pull_request: + paths: + - ".github/workflows/kuksa_go_client.yaml" + - "kuksa_go_client/**" + workflow_dispatch: + +concurrency: + group: ${{ github.ref }}-${{ github.workflow }} + cancel-in-progress: true + +jobs: + + kuksa-go-client-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout kuksa-incubation + uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: 'kuksa_go_client/go.mod' + cache-dependency-path: 'kuksa_go_client/go.sum' + - run: go version + - name: Run go tests + run: | + cd kuksa_go_client + # We cannot use sudo apt install protobuf-compiler + # as default in Ubuntu 22.04 (3.12) consider optional as experimental feature + go run protocInstall/protocInstall.go + export PATH=$PATH:$HOME/protoc/bin + sudo chmod +x $HOME/protoc/bin/protoc + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + export PATH=$PATH:$HOME/go/bin + go generate . + go test . + - name: golangci-lint + uses: golangci/golangci-lint-action@v4 + with: + version: latest + working-directory: kuksa_go_client + skip-pkg-cache: true diff --git a/NOTICE.md b/NOTICE.md new file mode 100644 index 0000000..30851f9 --- /dev/null +++ b/NOTICE.md @@ -0,0 +1,25 @@ +# Notices for Eclipse Kuksa + +This content is produced and maintained by the Eclipse Kuksa project. + +* Project home: https://projects.eclipse.org/projects/automotive.kuksa + +## Trademarks + + Eclipse Kuksa is a trademark of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. For +more information regarding authorship of content, please consult the listed +source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Apache License, Version 2.0 + +You may obtain a copy of the License at +http://www.apache.org/licenses/LICENSE-2.0 + +SPDX-License-Identifier: Apache-2.0 diff --git a/README.md b/README.md index 6e0bc0a..dd44329 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Component | Content | Comment/Status [Seat Service](seat_service) | C++ service example [eCAL Provider](ecal2val) | Python provider for [eCAL](https://projects.eclipse.org/projects/automotive.ecal) [PS4/PS5 - 2021 Formula Provider](./fone2val) | F1 Telemetrydata source for [KUKSA Databroker](https://github.com/eclipse/kuksa.val/tree/master/kuksa_databroker) +[KUKSA GO Client](kuksa_go_client) | Example client written in the [GO](https://go.dev/) programming language for easy interaction with KUKSA Databroker and Server ## Contribution diff --git a/kuksa_go_client/.gitignore b/kuksa_go_client/.gitignore new file mode 100644 index 0000000..bd461b4 --- /dev/null +++ b/kuksa_go_client/.gitignore @@ -0,0 +1,3 @@ +kuksa_go_client +.vscode +*.zip diff --git a/kuksa_go_client/README.md b/kuksa_go_client/README.md new file mode 100644 index 0000000..e6c6b2e --- /dev/null +++ b/kuksa_go_client/README.md @@ -0,0 +1,101 @@ +# Running the KUKSA golang Client + +## Execute the example + +### Set everything up for the KUKSA.val GO client +- If you do not have GO installed follow this [page](https://go.dev/doc/install) and install v1.18 or above +- If you do not have a protobuf compiler installed execute the following from this directory: +``` +> go run protocInstall/protocInstall.go +``` +Or install the protobuf compiler yourself(https://grpc.io/docs/protoc-installation/) +- Add the protobuf compiler (e.g. HOME_DIR/protoc/bin) to your PATH variable. For example for linux do: +``` +> export PATH=$PATH:$HOME/protoc/bin +``` + +If using a newer Linux version that has protoc >= 3.19 by default you may alternatively install the protobuf compiler with apt: + +``` +> sudo apt install protobuf-compiler +``` + +if you use `apt`you must manually create a folder called `proto` + + +``` +> mkdir proto +``` + +- Run the following command to install the needed GO protocol buffers plugins: +``` +> go install google.golang.org/protobuf/cmd/protoc-gen-go@latest +> go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest +``` +The plugins will be installed in $GOBIN, defaulting to $GOPATH/bin which is default HOME_DIR/go/bin. It must be in your $PATH for the protocol compiler protoc to find it. +For linux execute: +``` +> export PATH=$PATH:$HOME/go/bin +``` +- Then execute +``` +> go generate . +``` +if you encounter a problem, you have to give the protoc executable the right to be executed e.g in Linux run +``` +> sudo chmod +x /protoc/bin/protoc +``` +### Run the KUKSA.val GO client +#### Start KUKSA.val Server or Databroker +- Build kuksa.val and start the server +``` +> cd kuksa.val/kuksa-val-server/build/src/ +> ./kuksa-val-server +``` +- Alternatively, start the appropriate docker container. +``` +> docker run -it --rm --net=host -p 127.0.0.1:8090:8090 -e LOG_LEVEL=ALL ghcr.io/eclipse/kuksa.val/kuksa-val:master +``` +- Build and run KUKSA.val Databroker by executing: +``` +> cargo run --bin databroker +``` +- Alternatively, start the apropriate docker container. +``` +> docker run -it --rm --net=host ghcr.io/eclipse/kuksa.val/databroker:master +``` +- To run the GO Client execute: +``` +> go build . +> go run . +``` +- Alternatively, execute: +``` +> ./kuksa_go_client +``` + +### Configuration of the KUKSA.val GO client +The GO clients reads the configuration file `kuksa-client.json`. In this repository example configurations for both +KUKSA.val Databroker (`kuksa-client-grpc.json`) and KUKSA.val Server (`kuksa-client-ws.json`) exists. +The file `kuksa-client.json` is by default linked to `kuksa-client-grpc.json`. + +For using the GO client with the kuksa.val server set protocol = ws and for a connection to kuksa.val Databroker set protocol = grpc. On the command line it's available through -protocol ws/grpc. + +*Note: For communication with KUKSA Databroker only insecure mode is supported, TLS can not be used!* + +### Dependency updates + +If dependencies needs to be updated the following commands can be used: + +``` +go generate . +go get -u +go mod tidy +``` + +This will update `go.mod`and `go.sum`. + +## Linters + +Our Continuous Integration verifies that the code pass the [Golang Linter](https://golangci-lint.run/usage/install). +To avoid failing PR builds it is recommended to run the linter manually before creating a Pull Request. diff --git a/kuksa_go_client/client_test.go b/kuksa_go_client/client_test.go new file mode 100644 index 0000000..a5a5ab7 --- /dev/null +++ b/kuksa_go_client/client_test.go @@ -0,0 +1,201 @@ +/******************************************************************************** +* Copyright (c) 2022 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package main + +import ( + "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/kuksa_client" + "testing" +) + +// Note: Go support two methods to write a string +// +// Using double quotes +// var myvar := "hello \"there\"" +// +// Using back quotes (raw strings) +// var myvar := `hello "there"` +// +// Single quotes only allowed for literals/runes, like 'w' + +func TestArrayParseNoQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say hello, abc]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != "say hello" { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayParseNoInsideQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say hello","abc"]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != "say hello" { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayParseNoInsideQuoteSingle(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`['say hello','abc']`) + if len(array) != 2 { + t.Fail() + } + if array[0] != "say hello" { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayParseDoubleQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say \"hello\"","abc"]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != `say "hello"` { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayParseSingleQuote(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say \'hello\',"abc"]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != `say 'hello'` { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayParseComma(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["say, hello","abc"]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != `say, hello` { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArraySquare(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`[say hello[], abc]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != `say hello[]` { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayEmptyStringQuoted(t *testing.T) { + + array, _ := kuksa_client.GetArrayFromInput[string](`["", abc]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != `` { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestArrayEmptyStringNotQuoted(t *testing.T) { + // First item shall be ignored + array, _ := kuksa_client.GetArrayFromInput[string](`[, abc]`) + if len(array) != 1 { + t.Fail() + } + if array[0] != "abc" { + t.Fail() + } +} + +func TestDoubleComma(t *testing.T) { + // In this case the middle item is ignored + array, _ := kuksa_client.GetArrayFromInput[string](`[def,, abc]`) + if len(array) != 2 { + t.Fail() + } + if array[0] != "def" { + t.Fail() + } + if array[1] != "abc" { + t.Fail() + } +} + +func TestQuotesInStringValues(t *testing.T) { + array, _ := kuksa_client.GetArrayFromInput[string](`["dtc1, dtc2", dtc3, \" dtc4, dtc4\"]`) + if len(array) != 4 { + t.Fail() + } + if array[0] != "dtc1, dtc2" { + t.Fail() + } + if array[1] != "dtc3" { + t.Fail() + } + if array[2] != "\" dtc4" { + t.Fail() + } + if array[3] != "dtc4\"" { + t.Fail() + } +} + +func TestQuotesInStringValues2(t *testing.T) { + array, _ := kuksa_client.GetArrayFromInput[string]("['dtc1, dtc2', dtc3, \" dtc4, dtc4\"]") + if len(array) != 3 { + t.Fail() + } + if array[0] != "dtc1, dtc2" { + t.Fail() + } + if array[1] != "dtc3" { + t.Fail() + } + if array[2] != " dtc4, dtc4" { + t.Fail() + } +} diff --git a/kuksa_go_client/go.mod b/kuksa_go_client/go.mod new file mode 100644 index 0000000..a5585f7 --- /dev/null +++ b/kuksa_go_client/go.mod @@ -0,0 +1,36 @@ +module github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client + +go 1.19 + +require ( + github.com/google/uuid v1.6.0 + github.com/gorilla/websocket v1.5.1 + github.com/spf13/viper v1.18.2 + github.com/stretchr/objx v0.5.2 + google.golang.org/grpc v1.62.1 + google.golang.org/protobuf v1.33.0 +) + +require ( + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.0 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 // indirect + golang.org/x/net v0.22.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/kuksa_go_client/go.sum b/kuksa_go_client/go.sum new file mode 100644 index 0000000..1b7d060 --- /dev/null +++ b/kuksa_go_client/go.sum @@ -0,0 +1,75 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +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/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= +github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81 h1:6R2FC06FonbXQ8pK11/PDFY6N6LWlf9KlzibaCapmqc= +golang.org/x/exp v0.0.0-20240318143956-a85f2c67cd81/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= +golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= +google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/kuksa_go_client/kuksa-client-grpc.json b/kuksa_go_client/kuksa-client-grpc.json new file mode 100644 index 0000000..b4197f8 --- /dev/null +++ b/kuksa_go_client/kuksa-client-grpc.json @@ -0,0 +1,8 @@ +{ + "serverAddress": "localhost", + "serverPort": 55555, + "insecure": true, + "certsDir": "tbd", + "TokenOrTokenfile": "tbd", + "protocol": "grpc" +} \ No newline at end of file diff --git a/kuksa_go_client/kuksa-client-ws.json b/kuksa_go_client/kuksa-client-ws.json new file mode 100644 index 0000000..d09eed2 --- /dev/null +++ b/kuksa_go_client/kuksa-client-ws.json @@ -0,0 +1,8 @@ +{ + "serverAddress": "localhost", + "serverPort": 8090, + "insecure": true, + "certsDir": "tbd", + "TokenOrTokenfile" : "tbd", + "protocol": "ws" +} diff --git a/kuksa_go_client/kuksa-client.json b/kuksa_go_client/kuksa-client.json new file mode 120000 index 0000000..b8d8cb2 --- /dev/null +++ b/kuksa_go_client/kuksa-client.json @@ -0,0 +1 @@ +kuksa-client-grpc.json \ No newline at end of file diff --git a/kuksa_go_client/kuksa_client/backend.go b/kuksa_go_client/kuksa_client/backend.go new file mode 100644 index 0000000..0941e4b --- /dev/null +++ b/kuksa_go_client/kuksa_client/backend.go @@ -0,0 +1,26 @@ +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package kuksa_client + +type KuksaBackend interface { + ConnectToKuksaVal() error + Close() + AuthorizeKuksaValConn(TokenOrTokenfile string) error + GetValueFromKuksaVal(path string, attr string) ([]interface{}, error) + SetValueFromKuksaVal(path string, value string, attr string) error + SubscribeFromKuksaVal(path string, attr string) (string, error) + UnsubscribeFromKuksaVal(id string) error + PrintSubscriptionMessages(id string) error + GetMetadataFromKuksaVal(path string) ([]interface{}, error) +} diff --git a/kuksa_go_client/kuksa_client/commn.go b/kuksa_go_client/kuksa_client/commn.go new file mode 100644 index 0000000..452c260 --- /dev/null +++ b/kuksa_go_client/kuksa_client/commn.go @@ -0,0 +1,225 @@ +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package kuksa_client + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "os" + "log" + "net/url" + "sync" + "time" + + "github.com/gorilla/websocket" + "github.com/stretchr/objx" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + + grpcpb "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/proto/kuksa/val/v1" +) + +type KuksaClientCommWs struct { + Config *KuksaClientConfig + connSocket *websocket.Conn + sendChannel chan []byte + requestState map[string]*chan []byte + requestStateMutex sync.RWMutex + subscriptionState map[string]*chan []byte + subscriptionStateMutex sync.RWMutex +} + +type KuksaClientCommGrpc struct { + Config *KuksaClientConfig + conn *grpc.ClientConn + client grpcpb.VALClient + cancel map[string]*context.CancelFunc + subsChannel map[string]*grpcpb.VAL_SubscribeClient + subsAttr string + subscriptionStateMutex sync.RWMutex + authorizationHeader string +} + +const ( + TIMEOUT = 1 * time.Second +) + +func (cg *KuksaClientCommGrpc) startCommunication() error { + var opts []grpc.DialOption + if !cg.Config.Insecure { + return errors.New("TLS not supported for communication with KUKSA Databroker!") + } + opts = append(opts, grpc.WithTransportCredentials(insecure.NewCredentials())) + serverUrl := cg.Config.ServerAddress + ":" + cg.Config.ServerPort + var err error + cg.conn, err = grpc.Dial(serverUrl, opts...) + if err != nil { + return err + } + cg.client = grpcpb.NewVALClient(cg.conn) + cg.cancel = make(map[string]*context.CancelFunc) + cg.subsChannel = make(map[string]*grpcpb.VAL_SubscribeClient) + log.Println("gRPC channel Connected") + return nil +} + +func (cc *KuksaClientCommWs) startCommunication() error { + + if cc.Config.Insecure { + // Open an insecure websocket + serverUrl := url.URL{Scheme: "ws", Host: cc.Config.ServerAddress + ":" + cc.Config.ServerPort} + log.Printf("Connecting to " + serverUrl.String()) + + // Connect to the Kuksa Websocket + var err error + cc.connSocket, _, err = websocket.DefaultDialer.Dial(serverUrl.String(), nil) + if err != nil { + log.Fatal("Connection error: ", err) + return err + } + log.Println(string(cc.connSocket.LocalAddr().String())) + } else { + // Open a secure websocket + serverUrl := url.URL{Scheme: "wss", Host: cc.Config.ServerAddress + ":" + cc.Config.ServerPort} + log.Printf("Connecting to " + serverUrl.String()) + + // Load Client cert + cert, err := tls.LoadX509KeyPair(cc.Config.CertsDir+"/Client.pem", cc.Config.CertsDir+"/Client.key") + if err != nil { + log.Fatal("Certificate Error: ", err) + } + + // Load CA Certificate + caCert, err := os.ReadFile(cc.Config.CertsDir + "/CA.pem") + if err != nil { + return err + } + caCertPool := x509.NewCertPool() + caCertPool.AppendCertsFromPEM(caCert) + + tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + RootCAs: caCertPool, + InsecureSkipVerify: true, + } + + dialer := websocket.Dialer{TLSClientConfig: tlsConfig} + cc.connSocket, _, err = dialer.Dial(serverUrl.String(), nil) + if err != nil { + log.Fatal("Connection error: ", err) + return err + } + log.Println(string(cc.connSocket.LocalAddr().String())) + } + + // Create send sub routine + cc.sendChannel = make(chan []byte, 10) + go func() { + for { + req, ok := <-cc.sendChannel + if ok { + err := cc.connSocket.WriteMessage(websocket.TextMessage, []byte(req)) + if err != nil { + log.Fatal("WriteMessage error: ", err) + } + } + } + }() + + // Create receive sub routine and the request state + cc.requestState = make(map[string]*chan []byte) + cc.subscriptionState = make(map[string]*chan []byte) + go func() { + for { + _, respString, err := cc.connSocket.ReadMessage() + + if err != nil { + return + } + resp, _ := objx.FromJSON(string(respString)) + // Get the correct channel to send back response + if resp.Has("requestId") { + cc.requestStateMutex.RLock() + respChannel, ok := cc.requestState[resp.Get("requestId").String()] + if ok { + select { + case *respChannel <- respString: + default: + continue + } + } + cc.requestStateMutex.RUnlock() + } else if resp.Get("action").String() == "subscription" { + cc.subscriptionStateMutex.RLock() + respChannel, ok := cc.subscriptionState[resp.Get("subscriptionId").String()] + if ok { + select { + case *respChannel <- []byte(resp.Get("data.dp.value").String()): + default: + continue + } + } + cc.subscriptionStateMutex.RUnlock() + } + + } + + }() + + return nil +} + +// Close the Kuksa Client +func (cg *KuksaClientCommGrpc) Close() { + defer cg.conn.Close() +} + +func (cc *KuksaClientCommWs) Close() { + close(cc.sendChannel) + cc.connSocket.Close() +} + +// Communication Handler +func (cc *KuksaClientCommWs) communicationHandler(req objx.Map) (objx.Map, error) { + // Create channel for response and store it in the Request state + respChannel := make(chan []byte) + defer close(respChannel) + cc.requestStateMutex.Lock() + cc.requestState[req.Get("requestId").String()] = &respChannel + cc.requestStateMutex.Unlock() + + reqString, _ := req.JSON() + cc.sendChannel <- []byte(reqString) + + timeOut := time.After(TIMEOUT) + var err error + var response objx.Map + select { + case responseString := <-respChannel: + response, _ = objx.FromJSON(string(responseString)) + if response.Has("error") { + errString, _ := response.Get("error").ObjxMap().JSON() + err = errors.New(errString) + } + case <-timeOut: + err = errors.New("Timeout") + } + + cc.requestStateMutex.Lock() + delete(cc.requestState, req.Get("requestId").String()) + cc.requestStateMutex.Unlock() + return response, err +} diff --git a/kuksa_go_client/kuksa_client/config.go b/kuksa_go_client/kuksa_client/config.go new file mode 100644 index 0000000..9f438aa --- /dev/null +++ b/kuksa_go_client/kuksa_client/config.go @@ -0,0 +1,66 @@ +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package kuksa_client + +import ( + "log" + "strconv" + + "github.com/spf13/viper" +) + +type KuksaClientConfig struct { + ServerAddress string `mapstructure:"serverAddress"` + ServerPort string `mapstructure:"serverPort"` + Insecure bool `mapstructure:"insecure"` + CertsDir string `mapstructure:"certsDir"` + TokenOrTokenfile string `mapstructure:"TokenOrTokenfile"` + TransportProtocol string `mapstructure:"protocol"` +} + +func ReadConfig(config *KuksaClientConfig) { + + // Read in the configuration of the switcher + log.Println("Reading Config ...") + + viper.SetConfigName("kuksa-client") // name of config file (without extension) + viper.AddConfigPath("./") // path to look for the config file in + + viper.SetEnvPrefix("kuksa_client") + viper.AutomaticEnv() + err := viper.ReadInConfig() // Find and read the config file + if err != nil { // Handle errors reading the config file + log.Panicf("Fatal error config file: %s \n", err) + } + + err = viper.Unmarshal(&config) + if err != nil { + log.Panicf("Unable to decode config into struct, %v", err) + } +} + +func (config KuksaClientConfig) String() string { + + var retString string + + retString += "\nKuksa VISS Client Config\n" + retString += "\tServer Address: " + config.ServerAddress + "\n" + retString += "\tServer Port: " + config.ServerPort + "\n" + retString += "\tInsecure: " + strconv.FormatBool(config.Insecure) + "\n" + retString += "\tCertsDir: " + config.CertsDir + "\n" + retString += "\tTokenOrTokenfile: " + config.TokenOrTokenfile + "\n" + retString += "\tTransport Protocol: " + string(config.TransportProtocol) + "\n" + + return retString +} diff --git a/kuksa_go_client/kuksa_client/grpc.go b/kuksa_go_client/kuksa_client/grpc.go new file mode 100644 index 0000000..aef8b03 --- /dev/null +++ b/kuksa_go_client/kuksa_client/grpc.go @@ -0,0 +1,469 @@ +/******************************************************************************** +* Copyright (c) 2022 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package kuksa_client + +import ( + "context" + "errors" + "log" + "os" + "reflect" + "regexp" + "strconv" + "strings" + "time" + + "github.com/google/uuid" + "google.golang.org/grpc/metadata" + + grpcpb "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/proto/kuksa/val/v1" + v1 "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/proto/kuksa/val/v1" +) + +type FieldView struct { + Field grpcpb.Field + View grpcpb.View +} + +type ParseError struct{} + +func (e ParseError) Error() string { + return "parse error" +} + +var dict map[string]FieldView + +func (cg *KuksaClientCommGrpc) ConnectToKuksaVal() error { + dict = map[string]FieldView{ + "value": {grpcpb.Field_FIELD_VALUE, grpcpb.View_VIEW_CURRENT_VALUE}, + "targetValue": {grpcpb.Field_FIELD_ACTUATOR_TARGET, grpcpb.View_VIEW_TARGET_VALUE}, + "metadata": {grpcpb.Field_FIELD_METADATA, grpcpb.View_VIEW_METADATA}, + } + err := cg.startCommunication() + return err +} + +// Function to get attribute value from databroker +func (cg *KuksaClientCommGrpc) GetValueFromKuksaVal(path string, attr string) ([]interface{}, error) { + + // Contact the server and return the Value + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", cg.authorizationHeader) + defer cancel() + view := dict[attr].View + fields := []grpcpb.Field{dict[attr].Field} + entries := grpcpb.EntryRequest{Path: path, View: view, Fields: fields} + request := []*grpcpb.EntryRequest{&entries} + resp, err := cg.client.Get(ctx, &grpcpb.GetRequest{Entries: request}) + if err != nil { + log.Fatalf("Get call failed: %v", err) + } + respEntries := resp.GetEntries() + response := make([]interface{}, len(respEntries)) + for i, entry := range respEntries { + response[i] = interface{}(entry) + } + return response, nil +} + +func GetArrayFromInput[T any](input string) ([]T, error) { + // Strip the brackets from the input + input = strings.TrimSuffix(strings.TrimPrefix(input, "["), "]") + + // Split the input string into separate values + // First alternative, not quotes including escaped double quote, ends at comma, single/double quote or EOL + // Second group is double quoted string, may contain double quoted strings inside, ends at non-escaped + // double quote + pattern := `(?:\\"|\\'|[^"',])+|"(?:\\"|[^"])*"|'(?:\\'|[^'])*'` + + r := regexp.MustCompile(pattern) + values := r.FindAllString(input, -1) + + // Parse each value as type T and append to the array + array := make([]T, 0) + for _, v := range values { + + v = strings.TrimSpace(v) + var consider = len(v) > 0 + // make the ' or " disappear + if len(v) > 1 && (v[0] == '"') && (v[len(v)-1] == '"') { + v = v[1:len(v)-1] + } + if len(v) > 1 && (v[0] == '\'') && (v[len(v)-1] == '\'') { + v = v[1:len(v)-1] + } + v = strings.ReplaceAll(v,"\\\"", "\"") + v = strings.ReplaceAll(v,"\\'", "'") + if consider { + value, err := parseValue[T](v) + if err != nil { + return nil, ParseError{} + } + array = append(array,value) + } + } + + return array, nil +} + +func parseValue[T any](value string) (T, error) { + t := reflect.TypeOf((*T)(nil)).Elem() + switch t.Kind() { + case reflect.Bool: + parsed, err := strconv.ParseBool(strings.TrimSpace(value)) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetBool(parsed) + return v, nil + case reflect.Int32: + parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 32) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetInt(parsed) + return v, nil + case reflect.Int64: + parsed, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetInt(parsed) + return v, nil + case reflect.Float32: + parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 32) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetFloat(parsed) + return v, nil + case reflect.Float64: + parsed, err := strconv.ParseFloat(strings.TrimSpace(value), 64) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetFloat(parsed) + return v, nil + case reflect.Uint32: + parsed, err := strconv.ParseUint(strings.TrimSpace(value), 10, 32) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetUint(parsed) + return v, nil + case reflect.Uint64: + parsed, err := strconv.ParseUint(strings.TrimSpace(value), 10, 64) + if err != nil { + return reflect.Zero(t).Interface().(T), err + } + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetUint(parsed) + return v, nil + case reflect.String: + v := reflect.New(t).Elem().Interface().(T) + reflect.ValueOf(&v).Elem().SetString(value) + return v, nil + default: + return reflect.Zero(t).Interface().(T), errors.New("unsupported type") + } +} + +func (cg *KuksaClientCommGrpc) SetValueFromKuksaVal(path string, value string, attr string) error { + // Contact the server and set the Value + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", cg.authorizationHeader) + defer cancel() + entries, getErr := cg.GetValueFromKuksaVal(path, "metadata") + if getErr != nil { + return getErr + } + + var datatype int32 + for _, entry := range entries { + metadata := entry.(*v1.DataEntry).GetMetadata() + datatype = int32(metadata.GetDataType().Number()) + } + + // because of the lack of int8/16 and uint8/16 there are multiple times Datapoint_U/Int32 in the switch case + // the switch case needs to be modified every time you add a new datapoint or datatype + // the solution has been chosen because the compiler needs to know the type of a variable at runtime + // second option would have been to have a setValue function for every type + // this option has been not chosen because firstly the user needs to know what datatype his path requires (user exxperience low) + // and secondly this would have been not less code only would have saved a little bit of runtime + // there are maybe other options + var datapoint grpcpb.Datapoint + switch datatype { + case 1: + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_String_{String_: value}} + case 2: + boolean, convErr := strconv.ParseBool(value) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Bool{Bool: boolean}} + case 3: + int32_, convErr := strconv.ParseInt(value, 10, 8) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32{Int32: int32(int32_)}} + case 4: + int32_, convErr := strconv.ParseInt(value, 10, 16) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32{Int32: int32(int32_)}} + case 5: + int32_, convErr := strconv.ParseInt(value, 10, 32) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32{Int32: int32(int32_)}} + case 6: + int64_, convErr := strconv.ParseInt(value, 10, 64) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int64{Int64: int64_}} + case 7: + uint32_, convErr := strconv.ParseUint(value, 10, 8) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32{Uint32: uint32(uint32_)}} + case 8: + uint32_, convErr := strconv.ParseUint(value, 10, 16) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32{Uint32: uint32(uint32_)}} + case 9: + uint32_, convErr := strconv.ParseUint(value, 10, 32) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32{Uint32: uint32(uint32_)}} + case 10: + uint64_, convErr := strconv.ParseUint(value, 10, 64) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint64{Uint64: uint64_}} + case 11: + float_, convErr := strconv.ParseFloat(value, 32) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Float{Float: float32(float_)}} + case 12: + float_, convErr := strconv.ParseFloat(value, 64) + if convErr != nil { + return convErr + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Double{Double: float_}} + case 20: + array, err := GetArrayFromInput[string](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_StringArray{StringArray: &grpcpb.StringArray{Values: array}}} + case 21: + array, err := GetArrayFromInput[bool](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_BoolArray{BoolArray: &grpcpb.BoolArray{Values: array}}} + case 22: + array, err := GetArrayFromInput[int32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} + case 23: + array, err := GetArrayFromInput[int32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} + case 24: + array, err := GetArrayFromInput[int32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int32Array{Int32Array: &grpcpb.Int32Array{Values: array}}} + case 25: + array, err := GetArrayFromInput[int64](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Int64Array{Int64Array: &grpcpb.Int64Array{Values: array}}} + case 26: + array, err := GetArrayFromInput[uint32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} + case 27: + array, err := GetArrayFromInput[uint32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} + case 28: + array, err := GetArrayFromInput[uint32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint32Array{Uint32Array: &grpcpb.Uint32Array{Values: array}}} + case 29: + array, err := GetArrayFromInput[uint64](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_Uint64Array{Uint64Array: &grpcpb.Uint64Array{Values: array}}} + case 30: + array, err := GetArrayFromInput[float32](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_FloatArray{FloatArray: &grpcpb.FloatArray{Values: array}}} + case 31: + array, err := GetArrayFromInput[float64](value) + if err != nil { + return err + } + datapoint = grpcpb.Datapoint{Value: &grpcpb.Datapoint_DoubleArray{DoubleArray: &grpcpb.DoubleArray{Values: array}}} + } + + fields := []grpcpb.Field{dict[attr].Field} + var entry grpcpb.DataEntry + if attr == "value" { + entry = grpcpb.DataEntry{Path: path, Value: &datapoint} + } else if attr == "targetValue" { + entry = grpcpb.DataEntry{Path: path, ActuatorTarget: &datapoint} + } + + requestargs := []*grpcpb.EntryUpdate{} + requestargs = append(requestargs, &grpcpb.EntryUpdate{Entry: &entry, Fields: fields}) + _, err := cg.client.Set(ctx, &grpcpb.SetRequest{Updates: requestargs}) + return err +} + +func (cg *KuksaClientCommGrpc) PrintSubscriptionMessages(id string) error { + for { + channel := *cg.subsChannel[id] + resp, err := channel.Recv() + if err != nil { + return err + } + updates := resp.GetUpdates() + log.Println(updates) + + for _, update := range updates { + var val string + entry := update.GetEntry() + switch cg.subsAttr { + case "value": + val = entry.GetValue().String() + case "targetValue": + val = entry.GetActuatorTarget().String() + case "metadata": + val = entry.GetMetadata().String() + default: + continue + } + log.Printf("Vehicle.Speed Subscribed: %s", val) + } + } +} + +func (cg *KuksaClientCommGrpc) SubscribeFromKuksaVal(path string, attr string) (string, error) { + // Contact the server and return SubscriptionId + cg.subsAttr = attr + ctx, cancel := context.WithCancel(context.Background()) + ctx = metadata.AppendToOutgoingContext(ctx, "Authorization", cg.authorizationHeader) + id := uuid.New().String() + cg.cancel[id] = &cancel + view := dict[attr].View + fields := []grpcpb.Field{dict[attr].Field} + subEntry := []*grpcpb.SubscribeEntry{{Path: path, View: view, Fields: fields}} + subReq := grpcpb.SubscribeRequest{Entries: subEntry} + client, err := cg.client.Subscribe(ctx, &subReq) + if err != nil { + return "", err + } + cg.subscriptionStateMutex.Lock() + cg.subsChannel[id] = &client + cg.subscriptionStateMutex.Unlock() + return id, nil +} + +func (cg *KuksaClientCommGrpc) UnsubscribeFromKuksaVal(id string) error { + cancel := *cg.cancel[id] + cancel() + client := *cg.subsChannel[id] + err := client.CloseSend() + if err != nil { + log.Fatal("Error with CloseSend: ", err) + return err + } + + return nil +} + +// Function to authorize to kuksa.val server +func (cg *KuksaClientCommGrpc) AuthorizeKuksaValConn(TokenOrTokenfile string) error { + var tokenString string + // Data in config has precedence over hardcoded token + if cg.Config.TokenOrTokenfile != ""{ + tokenString = cg.Config.TokenOrTokenfile + }else{ + if TokenOrTokenfile == "" { + return errors.New("No token given!") + } + tokenString = TokenOrTokenfile + } + + log.Printf("Using token: %s", tokenString) + + info, err := os.Stat(tokenString) + if err != nil { + // the TokenOrTokenfile is read like its a token + } else if info.Mode().IsRegular() { + tokenByteString, err := os.ReadFile(tokenString) + tokenString = string(tokenByteString) + if err != nil { + log.Fatal("Error reading token: ", err) + return err + } + } + cg.authorizationHeader = "Bearer " + tokenString + return nil +} + +// Function to get metadata from kuksa.val server +func (cg *KuksaClientCommGrpc) GetMetadataFromKuksaVal(path string) ([]interface{}, error) { + metadata, err := cg.GetValueFromKuksaVal(path, "metadata") + if err != nil { + metadata = []interface{}{} + return metadata, err + } + return metadata, nil +} diff --git a/kuksa_go_client/kuksa_client/ws.go b/kuksa_go_client/kuksa_client/ws.go new file mode 100644 index 0000000..7e40e79 --- /dev/null +++ b/kuksa_go_client/kuksa_client/ws.go @@ -0,0 +1,183 @@ +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +package kuksa_client + +import ( + "errors" + "log" + "os" + + "github.com/google/uuid" + "github.com/stretchr/objx" +) + +// Function to connect to kuksa.val +func (cc *KuksaClientCommWs) ConnectToKuksaVal() error { + err := cc.startCommunication() + log.Println("Websocket connected") + return err +} + +// Function to get attribute value from VSS tree +func (cc *KuksaClientCommWs) GetValueFromKuksaVal(path string, attr string) ([]interface{}, error) { + + // Create new KuksaValRequest + req := objx.New(make(map[string]interface{})) + req.Set("requestId", uuid.New().String()) + req.Set("action", "get") + req.Set("path", path) + req.Set("attribute", attr) + + resp, err := cc.communicationHandler(req) + var value string + if resp.Has("data.dp." + attr) { + value = resp.Get("data.dp." + attr).String() + } + values := []interface{}{} + values = append(values, value) + return values, err +} + +func (cc *KuksaClientCommWs) PrintSubscriptionMessages(id string) error { + for { + val := <-*cc.subscriptionState[id] + log.Printf("Vehicle.Speed Subscribed: %s", val) + } +} + +// Function to subscribe attribute value from VSS tree +func (cc *KuksaClientCommWs) SubscribeFromKuksaVal(path string, attr string) (string, error) { + + // Create new channel for subscribing + id := uuid.New().String() + subsChannel := make(chan []byte, 10) + + // Create new KuksaValRequest + req := objx.New(make(map[string]interface{})) + req.Set("requestId", id) + req.Set("action", "subscribe") + req.Set("path", path) + req.Set("attribute", attr) + + // Create channel for subscription + resp, err := cc.communicationHandler(req) + var subscriptionId string + if err == nil { + subscriptionId = resp.Get("subscriptionId").String() + cc.subscriptionStateMutex.Lock() + cc.subscriptionState[subscriptionId] = &subsChannel + cc.subscriptionStateMutex.Unlock() + + } + return subscriptionId, err +} + +// Function to unsubscribe value from VSS tree +func (cc *KuksaClientCommWs) UnsubscribeFromKuksaVal(id string) error { + + // Create new KuksaValRequest + req := objx.New(make(map[string]interface{})) + req.Set("requestId", uuid.New().String()) + req.Set("action", "unsubscribe") + req.Set("subscriptionId", id) + + _, err := cc.communicationHandler(req) + if err == nil { + cc.subscriptionStateMutex.Lock() + delete(cc.subscriptionState, id) + cc.subscriptionStateMutex.Unlock() + } + + close(*cc.subscriptionState[id]) + return err +} + +// Function to set value from VSS tree +func (cc *KuksaClientCommWs) SetValueFromKuksaVal(path string, value string, attr string) error { + + // Create new KuksaValRequest + respChannel := make(chan objx.Map) + defer close(respChannel) + req := objx.New(make(map[string]interface{})) + uuid := uuid.New().String() + req.Set("requestId", uuid) + req.Set("action", "set") + req.Set("path", path) + req.Set("attribute", attr) + // Note: Line below currently gives problems if value is a string representation of a JSON object + // (array and theoretically also struct) + // The Set method handles it as a string, which the gives problems at the server side which expects for example + // a JSON array rather than a string containing an array + req.Set(attr, value) + + _, err := cc.communicationHandler(req) + return err +} + +// Function to authorize to kuksa.val server +func (cc *KuksaClientCommWs) AuthorizeKuksaValConn(TokenOrTokenfile string) error { + + var tokenString string + // Data in config has precedence over hardcoded token + if cc.Config.TokenOrTokenfile != ""{ + tokenString = cc.Config.TokenOrTokenfile + }else{ + if TokenOrTokenfile == "" { + return errors.New("No token given!") + } + tokenString = TokenOrTokenfile + } + + log.Printf("Using token: %s", tokenString) + + info, err := os.Stat(tokenString) + if err != nil { + // the TokenOrTokenfile is read like its a token + } else if info.Mode().IsRegular() { + tokenByteString, err := os.ReadFile(tokenString) + tokenString = string(tokenByteString) + if err != nil { + log.Fatal("Error reading token: ", err) + return err + } + } + + // Create new KuksaValRequest + req := objx.New(make(map[string]interface{})) + req.Set("requestId", uuid.New().String()) + req.Set("action", "authorize") + req.Set("tokens", tokenString) + + _, err = cc.communicationHandler(req) + return err +} + +// Function to get metadata from kuksa.val server +func (cc *KuksaClientCommWs) GetMetadataFromKuksaVal(path string) ([]interface{}, error) { + + // Create new KuksaValRequest + req := objx.New(make(map[string]interface{})) + req.Set("requestId", uuid.New().String()) + req.Set("action", "getMetaData") + req.Set("path", path) + + resp, err := cc.communicationHandler(req) + var value string + if resp.Has("metadata") { + value, err = resp.Get("metadata").ObjxMap().JSON() + } + values := []interface{}{} + values = append(values, value) + return values, err +} diff --git a/kuksa_go_client/main.go b/kuksa_go_client/main.go new file mode 100644 index 0000000..05842aa --- /dev/null +++ b/kuksa_go_client/main.go @@ -0,0 +1,226 @@ +// Copyright 2023 Robert Bosch GmbH +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ + +//go:generate protoc --go_out=proto --go-grpc_out=proto -I proto kuksa/val/v1/types.proto +//go:generate protoc --go_out=proto --go-grpc_out=proto -I proto kuksa/val/v1/val.proto + +package main + +import ( + "flag" + "log" + + "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/kuksa_client" + v1 "github.com/eclipse-kuksa/kuksa-incubation/kuksa_go_client/proto/kuksa/val/v1" +) + +func main() { + log.Println("Starting Kuksa Client!!") + + // Load the Kuksa Configuration + var configKuksaClient kuksa_client.KuksaClientConfig + kuksa_client.ReadConfig(&configKuksaClient) + protocol := flag.String("protocol", "undefined", "Could be grpc or ws. Per default the protocol variable is undefined. Then the value from the config file kuksa_client.json will be used.") + flag.Parse() + if *protocol == "undefined" { + protocol = &configKuksaClient.TransportProtocol + } + + var backend kuksa_client.KuksaBackend + + var token string + if *protocol == "ws" { + // example token from kuksa.val/kuksa_certificates/jwt/all-read-write.json.token + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJrdWtzYS52YWwiLCJpc3MiOiJFY2xpcHNlIEtVS1NBIERldiIsImFkbWluIjp0cnVlLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTc2NzIyNTU5OSwia3Vrc2EtdnNzIjp7IioiOiJydyJ9fQ.QQcVR0RuRJIoasPXYsMGZhdvhLjUalk4GcRaxhh3-0_j3CtVSZ0lTbv_Z3As5BfIYzaMlwUzFGvCVOq2MXVjRK81XOAZ6wIsyKOxva16zjbZryr2V_m3yZ4twI3CPEzJch11_qnhInirHltej-tGg6ySfLaTYeAkw4xYGwENMBBhN5t9odANpScZP_xx5bNfwdW1so6FkV1WhpKlCywoxk_vYZxo187d89bbiu-xOZUa5D-ycFkd1-1rjPXLGE_g5bc4jcQBvNBc-5FDbvt4aJlTQqjpdeppxhxn_gjkPGIAacYDI7szOLC-WYajTStbksUju1iQCyli11kPx0E66me_ZVwOX07f1lRF6D2brWm1LcMAHM3bQUK0LuyVwWPxld64uSAEsvSKsRyJERc7nZUgLf7COnUrrkxgIUNjukbdT2JVN_I-3l3b4YXg6JVD7Y5g0QYBKgXEFpZrDbBVhzo7PXPAhJD6-c3DcUQyRZExbrnFV56RwWuExphw8lYnbMvxPWImiVmB9nRVgFKD0TYaw1sidPSSlZt8Uw34VZzHWIZQAQY0BMjR33fefg42XQ1YzIwPmDx4GYXLl7HNIIVbsRsibKaJnf49mz2qnLC1K272zXSPljO11Ke1MNnsnKyUH7mcwEs9nhTsnMgEOx_TyMLRYo-VEHBDLuEOiBo" + backend = &kuksa_client.KuksaClientCommWs{Config: &configKuksaClient} + } else if *protocol == "grpc" { + // example token from kuksa.val/jwt/provide-all.token + // and https://github.com/eclipse-kuksa/kuksa-common/blob/main/jwt/provide-all.token + token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJsb2NhbCBkZXYiLCJpc3MiOiJjcmVhdGVUb2tlbi5weSIsImF1ZCI6WyJrdWtzYS52YWwiXSwiaWF0IjoxNTE2MjM5MDIyLCJleHAiOjE3NjcyMjU1OTksInNjb3BlIjoicHJvdmlkZSJ9.OJWzTvDjcmeWyg3vmBR5TEtqYaHq8HrpFLlTKZAfDBAQBUHpyUEboJ97jfWuWgBnTpnfboyfAbwvLqo6bEVZ6tXzF8n9LtW6HmPbIWoDqXuobM2grUCVaGKuOcnCpMCQYChziqHbYwRJYP9nkYgbQU1kE4dN7880Io4xzq0GEbWksB2CVpOoExQUmCZpCohPs-XEkdmXhcUKnWnOeiSsRGKusx987vpY_WOXh6WE7DfJgzAgpPDo33qI7zQuTzUILORQsiHmsrQO0-zcvokNjaQUzlt5ETZ7MQLCtiUQaN0NMbDMCWkmSfNvZ5hKCNbfr2FaiMzrGBOQdvQiFo-DqZKGNweaGpufYXuaKfn3SXKoDr8u1xDE5oKgWMjxDR9pQYGzIF5bDXITSywCm4kN5DIn7e2_Ga28h3rBl0t0ZT0cwlszftQRueDTFcMns1u9PEDOqf7fRrhjq3zqpxuMAoRANVd2z237eBsS0AvdSIxL52N4xO8P_h93NN8Vaum28fTPxzm8p9WlQh4mgUelggtT415hLcxizx15ARIRG0RiW91Pglzt4WRtXHnsg93Ixd3yXXzZ2i4Y0hqhj_L12SsXunK2VxKup2sFCQz6wM-t_7ADmNYcs80idzsadY8rYKDV8N1WqOOd4ANG_nzWa86Tyu6wAwhDVag5nbFmLZQ" + backend = &kuksa_client.KuksaClientCommGrpc{Config: &configKuksaClient} + } else { + log.Println("Specify -protocol=ws or -protocol=grpc") + } + + err := backend.ConnectToKuksaVal() + if err != nil { + log.Fatalf("Connection Error: %v", err) + } + defer backend.Close() + + // Authorize the connection + err = backend.AuthorizeKuksaValConn(token) + if err != nil { + log.Fatalf("Authorization Error: %v", err) + } + + err = backend.SetValueFromKuksaVal("Vehicle.ADAS.ABS.IsEnabled", "true", "value") + if err != nil { + log.Printf("Set Value Error: %v", err) + } else { + log.Printf("Vehicle.ADAS.ABS.IsEnabled Set: true") + } + + values, err := backend.GetValueFromKuksaVal("Vehicle.ADAS.ABS.IsEnabled", "value") + if err != nil { + log.Printf("Get Value Error: %v", err) + } else { + for _, value := range values { + if *protocol == "grpc" { + log.Println("Vehicle.ADAS.ABS.IsEnabled: " + value.(*v1.DataEntry).String()) + } else { + log.Println("Vehicle.ADAS.ABS.IsEnabled: " + value.(string)) + } + + } + } + + // Go client does not support setting of array values for Websocket + // Reason is SetValueFromKuksaVal where we set the JSON array we get as onput as string, + // so it gets quoted and considered as a string on server side and cause error + if *protocol == "grpc" { + err = backend.SetValueFromKuksaVal("Vehicle.OBD.DTCList", "[dtc1, dtc2, dtc3]", "value") + if err != nil { + log.Printf("Set Value Error: %v", err) + } else { + log.Printf("Vehicle.OBD.DTCList Set: [dtc1, dtc2, dtc3]") + } + + values, err = backend.GetValueFromKuksaVal("Vehicle.OBD.DTCList", "value") + if err != nil { + log.Printf("Get Value Error: %v", err) + } else { + for _, value := range values { + if *protocol == "grpc" { + log.Println("Vehicle.OBD.DTCList: " + value.(*v1.DataEntry).String()) + } else { + log.Println("Vehicle.OBD.DTCList: " + value.(string)) + } + } + } + + // set string with "" and \" + // Expected result is 4 items in the list + // dtc1, dtc2 + // dtc2 + // dtc3, dtc3 + // dtc4 + var valstr = "['dtc1, dtc2', dtc2, \"dtc3, dtc3\", dtc4]" + err = backend.SetValueFromKuksaVal("Vehicle.OBD.DTCList", valstr, "value") + if err != nil { + log.Printf("Set Value Error: %v", err) + } else { + log.Printf("Vehicle.OBD.DTCList Set: " + valstr) + } + + values, err = backend.GetValueFromKuksaVal("Vehicle.OBD.DTCList", "value") + if err != nil { + log.Printf("Get Value Error: %v", err) + } else { + for _, value := range values { + if *protocol == "grpc" { + log.Println("Vehicle.OBD.DTCList: " + value.(*v1.DataEntry).String()) + } else { + log.Println("Vehicle.OBD.DTCList: " + value.(string)) + } + } + } + } + err = backend.SetValueFromKuksaVal("Vehicle.ADAS.ABS.IsEnabled", "true", "targetValue") + if err != nil { + log.Printf("Set Value Error: %v", err) + } else { + log.Printf("Vehicle.ADAS.ABS.IsEnabled Set: true") + } + + tValues, err := backend.GetValueFromKuksaVal("Vehicle.ADAS.ABS.IsEnabled", "targetValue") + if err != nil { + log.Printf("Get Value Error: %v", err) + } else { + for _, value := range tValues { + if *protocol == "grpc" { + log.Println("Vehicle.ADAS.ABS.IsEnabled: " + value.(*v1.DataEntry).String()) + } else { + log.Println("Vehicle.ADAS.ABS.IsEnabled: " + value.(string)) + } + } + } + + // Get MetaData of Vehicle.Speed + value, err := backend.GetMetadataFromKuksaVal("Vehicle.Speed") + if err == nil { + for _, val := range value { + if *protocol == "grpc" { + log.Println("Vehicle.Speed Metadata: " + val.(*v1.DataEntry).String()) + } else { + log.Println("Vehicle.Speed Metadata: " + val.(string)) + } + } + } else { + log.Printf("Error while getting metadata: %s", err) + } + + //Subscribe to Vehicle.Speed + + id, err := backend.SubscribeFromKuksaVal("Vehicle.Speed", "value") + if err == nil { + log.Printf("Vehicle.Speed Subscription Id: %s", id) + } else { + log.Printf("Subscription Error %s", err) + } + err = backend.PrintSubscriptionMessages(id) + if err != nil { + log.Printf("Printing the subscription messages failed with: %s", err) + } +} + +// More subscribing examples +// idT, err := backend.SubscribeFromKuksaVal("Vehicle.Speed", "targetValue") +// if err == nil { +// log.Printf("Vehicle.Speed Subscription Id: %s", idT) +// } else { +// log.Printf("Subscription Error %s", err) +// } +// err = backend.PrintSubscriptionMessages(idT) +// if err != nil { +// log.Printf("Printing the subscription messages failed with: %s", err) +// } + +// err = backend.UnsubscribeFromKuksaVal(idT) +// if err != nil { +// log.Printf("Unsubscribing failed with: %s", err) +// } + +// err = backend.UnsubscribeFromKuksaVal(id) +// if err != nil { +// log.Printf("Unsubscribing failed with: %s", err) +// } + +// idM, err := backend.SubscribeFromKuksaVal("Vehicle.Speed", "metadata") +// if err == nil { +// log.Printf("Vehicle.Speed Subscription Id: %s", idM) +// } else { +// log.Printf("Subscription Error %s", err) +// } +// err = backend.PrintSubscriptionMessages(idM) +// if err != nil { +// log.Printf("Printing the subscription messages failed with: %s", err) +// } + +// err = backend.UnsubscribeFromKuksaVal(idM) +// if err != nil { +// log.Printf("Unsubscribing failed with: %s", err) +// } +// } +// } diff --git a/kuksa_go_client/proto/kuksa/val/v1/.gitignore b/kuksa_go_client/proto/kuksa/val/v1/.gitignore new file mode 100644 index 0000000..e796b66 --- /dev/null +++ b/kuksa_go_client/proto/kuksa/val/v1/.gitignore @@ -0,0 +1 @@ +*.go diff --git a/kuksa_go_client/proto/kuksa/val/v1/README.md b/kuksa_go_client/proto/kuksa/val/v1/README.md new file mode 100644 index 0000000..0f19980 --- /dev/null +++ b/kuksa_go_client/proto/kuksa/val/v1/README.md @@ -0,0 +1,5 @@ +# kuksa.val.v1 protobuf API + +This directory contains copies of kuksa.val.v1 Proto files. +The files here are supposed to match the original files stored in +[eclipse-kuksa/kuksa-databroker](https://github.com/eclipse-kuksa/kuksa-databroker/tree/main/proto/kuksa/val/v1). diff --git a/kuksa_go_client/proto/kuksa/val/v1/types.proto b/kuksa_go_client/proto/kuksa/val/v1/types.proto new file mode 100644 index 0000000..8914e7a --- /dev/null +++ b/kuksa_go_client/proto/kuksa/val/v1/types.proto @@ -0,0 +1,288 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +// I added V1 as in databroker. Is this good practice? +package kuksa.val.v1; +import "google/protobuf/timestamp.proto"; + +option go_package = "kuksa/val/v1"; + +// Describes a VSS entry +// When requesting an entry, the amount of information returned can +// be controlled by specifying either a `View` or a set of `Field`s. +message DataEntry { + // Defines the full VSS path of the entry. + string path = 1; // [field: FIELD_PATH] + + // The value (datapoint) + Datapoint value = 2; // [field: FIELD_VALUE] + + // Actuator target (only used if the entry is an actuator) + Datapoint actuator_target = 3; // [field: FIELD_ACTUATOR_TARGET] + + // Metadata for this entry + Metadata metadata = 10; // [field: FIELD_METADATA] +} + +message Datapoint { + google.protobuf.Timestamp timestamp = 1; + + oneof value { + string string = 11; + bool bool = 12; + sint32 int32 = 13; + sint64 int64 = 14; + uint32 uint32 = 15; + uint64 uint64 = 16; + float float = 17; + double double = 18; + StringArray string_array = 21; + BoolArray bool_array = 22; + Int32Array int32_array = 23; + Int64Array int64_array = 24; + Uint32Array uint32_array = 25; + Uint64Array uint64_array = 26; + FloatArray float_array = 27; + DoubleArray double_array = 28; + } +} + +message Metadata { + // Data type + // The VSS data type of the entry (i.e. the value, min, max etc). + // + // NOTE: protobuf doesn't have int8, int16, uint8 or uint16 which means + // that these values must be serialized as int32 and uint32 respectively. + DataType data_type = 11; // [field: FIELD_METADATA_DATA_TYPE] + + // Entry type + EntryType entry_type = 12; // [field: FIELD_METADATA_ENTRY_TYPE] + + // Description + // Describes the meaning and content of the entry. + optional string description = 13; // [field: FIELD_METADATA_DESCRIPTION] + + // Comment [optional] + // A comment can be used to provide additional informal information + // on a entry. + optional string comment = 14; // [field: FIELD_METADATA_COMMENT] + + // Deprecation [optional] + // Whether this entry is deprecated. Can contain recommendations of what + // to use instead. + optional string deprecation = 15; // [field: FIELD_METADATA_DEPRECATION] + + // Unit [optional] + // The unit of measurement + optional string unit = 16; // [field: FIELD_METADATA_UNIT] + + // Value restrictions [optional] + // Restrict which values are allowed. + // Only restrictions matching the DataType {datatype} above are valid. + ValueRestriction value_restriction = 17; // [field: FIELD_METADATA_VALUE_RESTRICTION] + + // Entry type specific metadata + oneof entry_specific { + Actuator actuator = 20; // [field: FIELD_METADATA_ACTUATOR] + Sensor sensor = 30; // [field: FIELD_METADATA_SENSOR] + Attribute attribute = 40; // [field: FIELD_METADATA_ATTRIBUTE] + } +} + +/////////////////////// +// Actuator specific fields +message Actuator { + // Nothing for now +} + +//////////////////////// +// Sensor specific +message Sensor { + // Nothing for now +} + +//////////////////////// +// Attribute specific +message Attribute { + // Nothing for now. +} + +// Value restriction +// +// One ValueRestriction{type} for each type, since +// they don't make sense unless the types match +// +message ValueRestriction { + oneof type { + ValueRestrictionString string = 21; + // For signed VSS integers + ValueRestrictionInt signed = 22; + // For unsigned VSS integers + ValueRestrictionUint unsigned = 23; + // For floating point VSS values (float and double) + ValueRestrictionFloat floating_point = 24; + } +} + +message ValueRestrictionInt { + optional sint64 min = 1; + optional sint64 max = 2; + repeated sint64 allowed_values = 3; +} + +message ValueRestrictionUint { + optional uint64 min = 1; + optional uint64 max = 2; + repeated uint64 allowed_values = 3; +} + +message ValueRestrictionFloat { + optional double min = 1; + optional double max = 2; + + // allowed for doubles/floats not recommended + repeated double allowed_values = 3; +} + +// min, max doesn't make much sense for a string +message ValueRestrictionString { + repeated string allowed_values = 3; +} + +// VSS Data type of a signal +// +// Protobuf doesn't support int8, int16, uint8 or uint16. +// These are mapped to int32 and uint32 respectively. +// +enum DataType { + DATA_TYPE_UNSPECIFIED = 0; + DATA_TYPE_STRING = 1; + DATA_TYPE_BOOLEAN = 2; + DATA_TYPE_INT8 = 3; + DATA_TYPE_INT16 = 4; + DATA_TYPE_INT32 = 5; + DATA_TYPE_INT64 = 6; + DATA_TYPE_UINT8 = 7; + DATA_TYPE_UINT16 = 8; + DATA_TYPE_UINT32 = 9; + DATA_TYPE_UINT64 = 10; + DATA_TYPE_FLOAT = 11; + DATA_TYPE_DOUBLE = 12; + DATA_TYPE_TIMESTAMP = 13; + DATA_TYPE_STRING_ARRAY = 20; + DATA_TYPE_BOOLEAN_ARRAY = 21; + DATA_TYPE_INT8_ARRAY = 22; + DATA_TYPE_INT16_ARRAY = 23; + DATA_TYPE_INT32_ARRAY = 24; + DATA_TYPE_INT64_ARRAY = 25; + DATA_TYPE_UINT8_ARRAY = 26; + DATA_TYPE_UINT16_ARRAY = 27; + DATA_TYPE_UINT32_ARRAY = 28; + DATA_TYPE_UINT64_ARRAY = 29; + DATA_TYPE_FLOAT_ARRAY = 30; + DATA_TYPE_DOUBLE_ARRAY = 31; + DATA_TYPE_TIMESTAMP_ARRAY = 32; +} + +// Entry type +enum EntryType { + ENTRY_TYPE_UNSPECIFIED = 0; + ENTRY_TYPE_ATTRIBUTE = 1; + ENTRY_TYPE_SENSOR = 2; + ENTRY_TYPE_ACTUATOR = 3; +} + +// A `View` specifies a set of fields which should +// be populated in a `DataEntry` (in a response message) +enum View { + VIEW_UNSPECIFIED = 0; // Unspecified. Equivalent to VIEW_CURRENT_VALUE unless `fields` are explicitly set. + VIEW_CURRENT_VALUE = 1; // Populate DataEntry with value. + VIEW_TARGET_VALUE = 2; // Populate DataEntry with actuator target. + VIEW_METADATA = 3; // Populate DataEntry with metadata. + VIEW_FIELDS = 10; // Populate DataEntry only with requested fields. + VIEW_ALL = 20; // Populate DataEntry with everything. +} + +// A `Field` corresponds to a specific field of a `DataEntry`. +// +// It can be used to: +// * populate only specific fields of a `DataEntry` response. +// * specify which fields of a `DataEntry` should be set as +// part of a `Set` request. +// * subscribe to only specific fields of a data entry. +// * convey which fields of an updated `DataEntry` have changed. +enum Field { + FIELD_UNSPECIFIED = 0; // "*" i.e. everything + FIELD_PATH = 1; // path + FIELD_VALUE = 2; // value + FIELD_ACTUATOR_TARGET = 3; // actuator_target + FIELD_METADATA = 10; // metadata.* + FIELD_METADATA_DATA_TYPE = 11; // metadata.data_type + FIELD_METADATA_DESCRIPTION = 12; // metadata.description + FIELD_METADATA_ENTRY_TYPE = 13; // metadata.entry_type + FIELD_METADATA_COMMENT = 14; // metadata.comment + FIELD_METADATA_DEPRECATION = 15; // metadata.deprecation + FIELD_METADATA_UNIT = 16; // metadata.unit + FIELD_METADATA_VALUE_RESTRICTION = 17; // metadata.value_restriction.* + FIELD_METADATA_ACTUATOR = 20; // metadata.actuator.* + FIELD_METADATA_SENSOR = 30; // metadata.sensor.* + FIELD_METADATA_ATTRIBUTE = 40; // metadata.attribute.* +} + +// Error response shall be an HTTP-like code. +// Should follow https://www.w3.org/TR/viss2-transport/#status-codes. +message Error { + uint32 code = 1; + string reason = 2; + string message = 3; +} + +// Used in get/set requests to report errors for specific entries +message DataEntryError { + string path = 1; // vss path + Error error = 2; +} + +message StringArray { + repeated string values = 1; +} + +message BoolArray { + repeated bool values = 1; +} + +message Int32Array { + repeated sint32 values = 1; +} + +message Int64Array { + repeated sint64 values = 1; +} + +message Uint32Array { + repeated uint32 values = 1; +} + +message Uint64Array { + repeated uint64 values = 1; +} + +message FloatArray { + repeated float values = 1; +} + +message DoubleArray { + repeated double values = 1; +} diff --git a/kuksa_go_client/proto/kuksa/val/v1/val.proto b/kuksa_go_client/proto/kuksa/val/v1/val.proto new file mode 100644 index 0000000..7fe5a93 --- /dev/null +++ b/kuksa_go_client/proto/kuksa/val/v1/val.proto @@ -0,0 +1,115 @@ +/******************************************************************************** + * Copyright (c) 2022 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License 2.0 which is available at + * http://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ + +syntax = "proto3"; + +package kuksa.val.v1; + +option go_package = "kuksa/val/v1"; + +import "kuksa/val/v1/types.proto"; + +// Note on authorization: +// Tokens (auth-token or auth-uuid) are sent as (GRPC / http2) metadata. +// +// The auth-token is a JWT compliant token as the examples found here: +// https://github.com/eclipse-kuksa/kuksa-databrokertree/master/certificates/jwt +// +// See also https://github.com/eclipse-kuksa/kuksa-databrokerblob/master/doc/jwt.md +// +// Upon reception of auth-token, server shall generate an auth-uuid in metadata +// that the client can use instead of auth-token in subsequent calls. + +service VAL { + // Get entries + rpc Get(GetRequest) returns (GetResponse); + + // Set entries + rpc Set(SetRequest) returns (SetResponse); + + // Subscribe to a set of entries + // + // Returns a stream of notifications. + // + // InvalidArgument is returned if the request is malformed. + rpc Subscribe(SubscribeRequest) returns (stream SubscribeResponse); + + // Shall return information that allows the client to determine + // what server/server implementation/version it is talking to + // eg. kuksa-databroker 0.5.1 + rpc GetServerInfo(GetServerInfoRequest) returns (GetServerInfoResponse); +} + +// Define which data we want +message EntryRequest { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Request a set of entries. +message GetRequest { + repeated EntryRequest entries = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message GetResponse { + repeated DataEntry entries = 1; + repeated DataEntryError errors = 2; + Error error = 3; +} + +// Define the data we want to set +message EntryUpdate { + DataEntry entry = 1; + repeated Field fields = 2; +} + +// A list of entries to be updated +message SetRequest { + repeated EntryUpdate updates = 1; +} + +// Global errors are specified in `error`. +// Errors for individual entries are specified in `errors`. +message SetResponse { + Error error = 1; + repeated DataEntryError errors = 2; +} + +// Define what to subscribe to +message SubscribeEntry { + string path = 1; + View view = 2; + repeated Field fields = 3; +} + +// Subscribe to changes in datapoints. +message SubscribeRequest { + repeated SubscribeEntry entries = 1; +} + +// A subscription response +message SubscribeResponse { + repeated EntryUpdate updates = 1; +} + +message GetServerInfoRequest { + // Nothing yet +} + +message GetServerInfoResponse { + string name = 1; + string version = 2; +} diff --git a/kuksa_go_client/protocInstall/protocInstall.go b/kuksa_go_client/protocInstall/protocInstall.go new file mode 100755 index 0000000..53a5f66 --- /dev/null +++ b/kuksa_go_client/protocInstall/protocInstall.go @@ -0,0 +1,185 @@ +/******************************************************************************** +* Copyright (c) 2023 Contributors to the Eclipse Foundation +* +* See the NOTICE file(s) distributed with this work for additional +* information regarding copyright ownership. +* +* This program and the accompanying materials are made available under the +* terms of the Apache License 2.0 which is available at +* http://www.apache.org/licenses/LICENSE-2.0 +* +* SPDX-License-Identifier: Apache-2.0 +********************************************************************************/ +package main + +import ( + "archive/zip" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + "strings" +) + +const ( + Name = "protoc" + ZipFileName = Name + ".zip" + OsMac = "darwin" + OsWindows = "windows" + OsLinux = "linux" +) + +func DownloadPackage(url string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + out, err := os.Create(ZipFileName) + if err != nil { + return err + } + defer out.Close() + + if _, err := io.Copy(out, resp.Body); err != nil { + return err + } + return nil +} + +func UnzipSource(source, destination string) error { + reader, err := zip.OpenReader(source) + if err != nil { + return err + } + defer reader.Close() + + destination, err = filepath.Abs(destination) + if err != nil { + return err + } + + for _, f := range reader.File { + if strings.Contains(f.Name, "..") { + fmt.Println("Ignoring path traversal elements (CWE-22): ", f.Name) + continue + } + filePath := filepath.Join(destination, f.Name) + if f.FileInfo().IsDir() { + err := os.MkdirAll(filePath, os.ModePerm) + if err != nil { + return err + } + continue + } else { + fmt.Println("unziping files to", filePath) + + out, err := os.Create(filePath) + if err != nil { + return err + } + defer out.Close() + + fileInArchive, err := f.Open() + if err != nil { + return err + } + + if _, err := io.Copy(out, fileInArchive); err != nil { + return err + } + } + } + + return nil +} + +var url = map[string]string{ + "linux_32": "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_32.zip", + "linux_64": "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-linux-x86_64.zip", + "darwin": "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-osx-x86_64.zip", + "windows_32": "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-win32.zip", + "windows_64": "https://github.com/protocolbuffers/protobuf/releases/download/v3.19.4/protoc-3.19.4-win64.zip", +} + +type NotFoundError struct{} + +func (m *NotFoundError) Error() string { + return "HOME was not found" +} + +func Install() error { + goos := runtime.GOOS + bit := 32 << (^uint(0) >> 63) + var downloadUrl string + switch goos { + case OsMac: + downloadUrl = url[OsMac] + case OsWindows: + downloadUrl = url[fmt.Sprintf("%s_%d", OsWindows, bit)] + case OsLinux: + downloadUrl = url[fmt.Sprintf("%s_%d", OsLinux, bit)] + default: + return fmt.Errorf("unsupport OS: %q", goos) + } + + print("Downloading protobuf compiler...\n") + err := DownloadPackage(downloadUrl) + if err != nil { + return err + } + + var home string + _, found := os.LookupEnv("HOME") + if found { + home = os.Getenv("HOME") + } else { + return &NotFoundError{} + } + + dest := filepath.Join(home, Name) + + print("Unziping source...") + return UnzipSource(ZipFileName, dest) +} + +func Exists() bool { + var root string + _, found := os.LookupEnv("HOME") + if found { + root = os.Getenv("HOME") + } else { + return false + } + protocPath := filepath.Join(root, Name) + if _, err := os.Stat(protocPath); os.IsNotExist(err) { + return false + } + + return true +} + +func ProtoExists() { + if _, err := os.Stat("proto"); os.IsNotExist(err) { + print("no directory proto exists. Creating...\n") + err := os.MkdirAll("proto", os.ModePerm) + if err != nil { + panic(err) + } + } +} + +func main() { + if !Exists() { + print("No installed protobuf compiler found! Installing... \n") + err := Install() + if err != nil { + panic(err) + } + } + ProtoExists() + print("All done!\n") +}