├── .dockerignore ├── examples ├── msgpackdump │ ├── testdata │ │ ├── array │ │ └── map │ └── main.go ├── ping_client │ └── main.go ├── ping_server │ └── main.go ├── README.md ├── generic_sock_client │ └── main.go └── mult_server │ └── main.go ├── debian ├── arduino-router │ ├── DEBIAN │ │ ├── postrm │ │ ├── postinst │ │ ├── prerm │ │ └── control │ └── etc │ │ └── systemd │ │ └── system │ │ ├── arduino-router-serial.service │ │ └── arduino-router.service └── Dockerfile ├── scripts └── run-remote-sudo.sh ├── dprint.json ├── .github ├── workflows │ ├── go-test.yml │ ├── block-merge-label.yml │ ├── release.yml │ ├── checks.yaml │ └── check-go-dependencies-task.yml └── PULL_REQUEST_TEMPLATE.md ├── .editorconfig ├── .gitignore ├── license_header.tpl ├── msgpackrpc ├── doc.go ├── README.md ├── type_conversion.go ├── logger.go ├── connection_rpc_test.go └── connection.go ├── .golangci.yml ├── .licenses └── arduino-router │ └── go │ ├── github.com │ ├── djherbis │ │ ├── buffer │ │ │ ├── limio.dep.yml │ │ │ └── wrapio.dep.yml │ │ ├── nio │ │ │ └── v3.dep.yml │ │ └── buffer.dep.yml │ ├── creack │ │ └── goselect.dep.yml │ ├── vmihailenco │ │ ├── msgpack │ │ │ ├── v5.dep.yml │ │ │ └── v5 │ │ │ │ └── msgpcode.dep.yml │ │ └── tagparser │ │ │ ├── v2.dep.yml │ │ │ └── v2 │ │ │ ├── internal.dep.yml │ │ │ └── internal │ │ │ └── parser.dep.yml │ └── spf13 │ │ ├── pflag.dep.yml │ │ └── cobra.dep.yml │ ├── go.bug.st │ ├── f.dep.yml │ ├── serial │ │ └── unixutils.dep.yml │ └── serial.dep.yml │ └── golang.org │ └── x │ └── sys │ └── unix.dep.yml ├── internal ├── msgpackrouter │ ├── errors.go │ ├── router_test.go │ └── router.go ├── monitorapi │ └── monitor-api.go ├── hciapi │ └── hci-api.go └── network-api │ ├── network-api_test.go │ └── network-api.go ├── .licensed.yml ├── go.mod ├── README.md ├── Taskfile.yml ├── main.go └── go.sum /.dockerignore: -------------------------------------------------------------------------------- 1 | .licenses/ 2 | .git/ 3 | .github/ 4 | -------------------------------------------------------------------------------- /examples/msgpackdump/testdata/array: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduino/arduino-router/main/examples/msgpackdump/testdata/array -------------------------------------------------------------------------------- /examples/msgpackdump/testdata/map: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arduino/arduino-router/main/examples/msgpackdump/testdata/map -------------------------------------------------------------------------------- /debian/arduino-router/DEBIAN/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ -d /run/systemd/system ]; then 4 | systemctl daemon-reload 5 | fi 6 | -------------------------------------------------------------------------------- /debian/arduino-router/DEBIAN/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl enable arduino-router 4 | systemctl enable arduino-router-serial 5 | 6 | -------------------------------------------------------------------------------- /debian/arduino-router/DEBIAN/prerm: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | systemctl disable arduino-router 4 | systemctl disable arduino-router-serial 5 | 6 | -------------------------------------------------------------------------------- /debian/arduino-router/DEBIAN/control: -------------------------------------------------------------------------------- 1 | Package: arduino-router 2 | Version: $VERSION 3 | Architecture: $ARCH 4 | Maintainer: arduino 5 | Description: Arduino application router 6 | Depends: socat, arduino-unoq-radio-firmware 7 | 8 | -------------------------------------------------------------------------------- /scripts/run-remote-sudo.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Runs a command as sudo on the board (adb shell), while prompting for the password interactively. 4 | # To be used in the taskfile. 5 | 6 | read -s -p "Enter device sudo password: " SUDO_PASS 7 | echo 8 | 9 | # Run the command remotely with sudo, piping the password 10 | echo "$SUDO_PASS" | adb shell "sudo -S sh -c \"$*\"" 11 | -------------------------------------------------------------------------------- /dprint.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown": {}, 3 | "dockerfile": {}, 4 | "yaml": {}, 5 | "excludes": [ 6 | ".licenses/**", 7 | "**/testdata/**" 8 | ], 9 | "plugins": [ 10 | "https://plugins.dprint.dev/markdown-0.17.8.wasm", 11 | "https://plugins.dprint.dev/dockerfile-0.3.2.wasm", 12 | "https://plugins.dprint.dev/g-plane/pretty_yaml-v0.5.0.wasm" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/go-test.yml: -------------------------------------------------------------------------------- 1 | name: Run Go Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | go-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout code 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up Go 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version-file: go.mod 20 | 21 | - name: Run tests 22 | run: go tool task test 23 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Default configuration for all files 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | # Use utf-8 charset for modern languages 15 | [*.{go}] 16 | charset = utf-8 17 | 18 | # Use tab indentation for Go and Makefiles 19 | [{*.go,go.*}] 20 | indent_style = tab 21 | indent_size = 4 22 | 23 | -------------------------------------------------------------------------------- /debian/arduino-router/etc/systemd/system/arduino-router-serial.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Proxy for the Arduino Router Monitor to ttyGS0 serial 3 | After=network-online.target arduino-router.service 4 | Wants=network-online.target arduino-router.service 5 | Requires=arduino-router.service 6 | 7 | [Service] 8 | ExecStart=/usr/bin/socat file:/dev/ttyGS0,raw,echo=0,b9600,crtscts=0 tcp:127.0.0.1:7500 9 | StandardOutput=journal 10 | StandardError=journal 11 | Restart=always 12 | RestartSec=3 13 | 14 | [Install] 15 | WantedBy=multi-user.target 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Motivation 2 | 3 | 4 | 5 | ### Change description 6 | 7 | 8 | 9 | ### Additional Notes 10 | 11 | 12 | 13 | ### Reviewer checklist 14 | 15 | - [ ] PR addresses a single concern. 16 | - [ ] PR title and description are properly filled. 17 | - [ ] Changes will be merged in `main`. 18 | - [ ] Changes are covered by tests. 19 | - [ ] Logging is meaningful in case of troubleshooting. 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | .vscode 17 | 18 | # Dependency directories (remove the comment below to include it) 19 | # vendor/ 20 | 21 | # Go workspace file 22 | go.work 23 | go.work.sum 24 | 25 | # local tools directory 26 | .bin/ 27 | 28 | build/ 29 | 30 | **/.cache 31 | 32 | -------------------------------------------------------------------------------- /debian/arduino-router/etc/systemd/system/arduino-router.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Arduino Router Service 3 | After=network-online.target 4 | Wants=network-online.target 5 | Requires= 6 | 7 | [Service] 8 | # Put the micro in a ready state. 9 | ExecStartPre=-/usr/bin/gpioset -c /dev/gpiochip1 -t0 37=0 10 | ExecStart=/usr/bin/arduino-router --unix-port /var/run/arduino-router.sock --serial-port /dev/ttyHS1 --serial-baudrate 115200 11 | # End the boot animation after the router is started. 12 | ExecStartPost=/usr/bin/gpioset -c /dev/gpiochip1 -t0 70=1 13 | StandardOutput=journal 14 | StandardError=journal 15 | Restart=always 16 | RestartSec=3 17 | 18 | [Install] 19 | WantedBy=multi-user.target 20 | -------------------------------------------------------------------------------- /.github/workflows/block-merge-label.yml: -------------------------------------------------------------------------------- 1 | name: Block Merge if "do-not-merge" Label Exists 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - opened 7 | - labeled 8 | - unlabeled 9 | - synchronize # important for when new commits are pushed 10 | 11 | jobs: 12 | check-do-not-merge-label: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check for "do-not-merge" label 16 | if: contains(github.event.pull_request.labels.*.name, 'do-not-merge') 17 | run: | 18 | echo "This Pull Request has the 'do-not-merge' label. Merging is blocked." 19 | echo "Please remove the 'do-not-merge' label to enable merging." 20 | exit 1 # This will cause the workflow to fail 21 | -------------------------------------------------------------------------------- /license_header.tpl: -------------------------------------------------------------------------------- 1 | This file is part of arduino-router 2 | 3 | Copyright{{ if .Year }} {{.Year}}{{ end }} {{.Holder}} 4 | 5 | This software is released under the GNU General Public License version 3, 6 | which covers the main part of arduino-router 7 | The terms of this license can be found at: 8 | https://www.gnu.org/licenses/gpl-3.0.en.html 9 | 10 | You can be released from the requirements of the above licenses by purchasing 11 | a commercial license. Buying such a license is mandatory if you want to 12 | modify or otherwise use the software for commercial activities involving the 13 | Arduino software without disclosing the source code of your own applications. 14 | To purchase a commercial license, send an email to license@arduino.cc. -------------------------------------------------------------------------------- /msgpackrpc/doc.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | // Package msgpack is an implementation of the MessagePack-RPC protocol, as 17 | // as defined in https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md 18 | package msgpackrpc 19 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - bodyclose 5 | - dogsled 6 | - goconst 7 | - gocritic 8 | - goprintffuncname 9 | - gosec 10 | - importas 11 | - misspell 12 | - nakedret 13 | - prealloc 14 | - revive 15 | - staticcheck 16 | - unconvert 17 | - unparam 18 | - gochecknoinits 19 | settings: 20 | misspell: 21 | locale: US 22 | revive: 23 | rules: 24 | - name: indent-error-flow 25 | disabled: true 26 | goconst: 27 | ignore-string-values: 28 | - ".exe" 29 | - "windows" 30 | - "linux" 31 | - "darwin" 32 | 33 | exclusions: 34 | generated: lax 35 | presets: 36 | - comments 37 | - common-false-positives 38 | - legacy 39 | - std-error-handling 40 | paths: 41 | - third_party$ 42 | - builtin$ 43 | - examples$ 44 | formatters: 45 | enable: 46 | - gofmt 47 | - goimports 48 | settings: 49 | goimports: 50 | local-prefixes: 51 | - github.com/arduino/arduino-router 52 | exclusions: 53 | generated: lax 54 | paths: 55 | - third_party$ 56 | - builtin$ 57 | - examples$ 58 | -------------------------------------------------------------------------------- /examples/msgpackdump/main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "log" 20 | "os" 21 | 22 | "github.com/arduino/go-paths-helper" 23 | "github.com/davecgh/go-spew/spew" 24 | "github.com/vmihailenco/msgpack/v5" 25 | ) 26 | 27 | func main() { 28 | f, err := paths.New(os.Args[1]).Open() 29 | if err != nil { 30 | log.Fatal(err) 31 | } 32 | d := msgpack.NewDecoder(f) 33 | for { 34 | if v, err := d.DecodeInterface(); err != nil { 35 | log.Fatal(err) 36 | } else { 37 | spew.Dump(v) 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release arduino-router 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v[0-9]+.[0-9]+.[0-9]+*" 7 | 8 | env: 9 | PROJECT_NAME: "arduino-router" 10 | GITHUB_TOKEN: ${{ secrets.ARDUINOBOT_TOKEN }} 11 | GITHUB_USERNAME: ArduinoBot 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | os: [ubuntu-22.04] 18 | arch: [amd64, arm64] 19 | 20 | runs-on: ${{ matrix.os }} 21 | 22 | steps: 23 | - name: Set env vars 24 | run: | 25 | echo "TAG_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV 26 | echo "creating tag ${TAG_VERSION}" 27 | 28 | - name: Checkout 29 | uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v5 35 | with: 36 | go-version-file: go.mod 37 | 38 | - name: Build deb 39 | run: | 40 | go tool task build-deb VERSION=${TAG_VERSION} ARCH=${{ matrix.arch }} RELEASE="true" 41 | 42 | - name: Create Github Release and upload artifacts 43 | uses: ncipollo/release-action@v1 44 | with: 45 | token: ${{ secrets.GITHUB_TOKEN }} 46 | draft: false 47 | prerelease: true 48 | artifacts: build/*.tar.gz,build/*.deb 49 | allowUpdates: true 50 | -------------------------------------------------------------------------------- /examples/ping_client/main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "net" 22 | 23 | "github.com/arduino/arduino-router/msgpackrpc" 24 | ) 25 | 26 | func main() { 27 | c, err := net.Dial("tcp", ":8900") 28 | if err != nil { 29 | panic(err) 30 | } 31 | 32 | conn := msgpackrpc.NewConnection(c, c, nil, nil, nil) 33 | defer conn.Close() 34 | go conn.Run() 35 | 36 | // Client 37 | reqResult, reqError, err := conn.SendRequest(context.Background(), "ping", "HELLO", 1, true, 5.0) 38 | if err != nil { 39 | panic(err) 40 | } 41 | fmt.Println(reqResult, reqError) 42 | } 43 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/djherbis/buffer/limio.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/djherbis/buffer/limio 3 | version: v1.2.0 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/djherbis/buffer/limio 7 | license: mit 8 | licenses: 9 | - sources: buffer@v1.2.0/LICENSE.txt 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2015 Dustin H 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 19 | the Software, and to permit persons to whom the Software is furnished to do so, 20 | subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 27 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 28 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 29 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 30 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | notices: [] 32 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/djherbis/buffer/wrapio.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/djherbis/buffer/wrapio 3 | version: v1.2.0 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/djherbis/buffer/wrapio 7 | license: mit 8 | licenses: 9 | - sources: buffer@v1.2.0/LICENSE.txt 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2015 Dustin H 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 19 | the Software, and to permit persons to whom the Software is furnished to do so, 20 | subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 27 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 28 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 29 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 30 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | notices: [] 32 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/djherbis/nio/v3.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/djherbis/nio/v3 3 | version: v3.0.1 4 | type: go 5 | summary: Package nio provides a few buffered io primitives. 6 | homepage: https://pkg.go.dev/github.com/djherbis/nio/v3 7 | license: mit 8 | licenses: 9 | - sources: LICENSE.txt 10 | text: | 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2015 Dustin H 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy of 16 | this software and associated documentation files (the "Software"), to deal in 17 | the Software without restriction, including without limitation the rights to 18 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 19 | the Software, and to permit persons to whom the Software is furnished to do so, 20 | subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 27 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 28 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 29 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 30 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | notices: [] 32 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/creack/goselect.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/creack/goselect 3 | version: v0.1.2 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/creack/goselect 7 | license: mit 8 | licenses: 9 | - sources: LICENSE 10 | text: |+ 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2014 Guillaume J. Charmes 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in all 23 | copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 31 | SOFTWARE. 32 | 33 | - sources: README.md 34 | text: Released under the [MIT license](LICENSE). 35 | notices: [] 36 | ... 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/djherbis/buffer.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/djherbis/buffer 3 | version: v1.2.0 4 | type: go 5 | summary: Package buffer implements a series of Buffers which can be composed to implement 6 | complicated buffering strategies 7 | homepage: https://pkg.go.dev/github.com/djherbis/buffer 8 | license: mit 9 | licenses: 10 | - sources: LICENSE.txt 11 | text: | 12 | The MIT License (MIT) 13 | 14 | Copyright (c) 2015 Dustin H 15 | 16 | Permission is hereby granted, free of charge, to any person obtaining a copy of 17 | this software and associated documentation files (the "Software"), to deal in 18 | the Software without restriction, including without limitation the rights to 19 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 20 | the Software, and to permit persons to whom the Software is furnished to do so, 21 | subject to the following conditions: 22 | 23 | The above copyright notice and this permission notice shall be included in all 24 | copies or substantial portions of the Software. 25 | 26 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 27 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 28 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 29 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 30 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 31 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | notices: [] 33 | -------------------------------------------------------------------------------- /.github/workflows/checks.yaml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | # In the same branch only 1 workflow per time can run. In case we're not in the 10 | # main branch we cancel previous running workflow 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 14 | 15 | permissions: 16 | contents: read 17 | # Used by the buf to create a comment with a brief summary of failing tets 18 | pull-requests: write 19 | 20 | jobs: 21 | run-checks: 22 | runs-on: ubuntu-latest 23 | steps: 24 | - uses: actions/checkout@v4 25 | 26 | - uses: dprint/check@v2.2 27 | with: 28 | dprint-version: 0.48.0 29 | 30 | - uses: golangci/golangci-lint-action@v8 31 | with: 32 | version: v2.4.0 33 | args: --timeout 300s 34 | 35 | - name: Check go mod 36 | run: | 37 | go mod tidy 38 | git diff --color --exit-code 39 | 40 | # This is required to allow licensee/setup-licensed to install Licensed via Ruby gem. 41 | - name: Install Ruby 42 | uses: ruby/setup-ruby@v1 43 | with: 44 | ruby-version: ruby # Install latest version 45 | - name: Install licensed 46 | uses: licensee/setup-licensed@v1.3.2 47 | with: 48 | github_token: ${{ secrets.GITHUB_TOKEN }} 49 | version: 5.x 50 | 51 | - name: Run deb copyright check 52 | run: | 53 | go tool task update-deb-copyright 54 | git diff --color --exit-code 55 | -------------------------------------------------------------------------------- /internal/msgpackrouter/errors.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrouter 17 | 18 | import "fmt" 19 | 20 | const ( 21 | // Error codes for the router 22 | ErrCodeInvalidParams = 1 23 | ErrCodeMethodNotAvailable = 2 24 | ErrCodeFailedToSendRequests = 3 25 | ErrCodeGenericError = 4 26 | ErrCodeRouteAlreadyExists = 5 27 | ) 28 | 29 | type RouteError struct { 30 | message string 31 | code int 32 | } 33 | 34 | func (m *RouteError) Error() string { 35 | return m.message 36 | } 37 | 38 | func (m *RouteError) ToEncodedError() []any { 39 | return []any{m.code, m.message} 40 | } 41 | 42 | func newRouteAlreadyExistsError(route string) *RouteError { 43 | return &RouteError{ 44 | message: fmt.Sprintf("route already exists: %s", route), 45 | code: ErrCodeRouteAlreadyExists, 46 | } 47 | } 48 | 49 | func routerError(code int8, message string) []any { 50 | return []any{code, message} 51 | } 52 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/vmihailenco/msgpack/v5.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/vmihailenco/msgpack/v5 3 | version: v5.4.1 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/vmihailenco/msgpack/v5 7 | license: bsd-2-clause 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | Copyright (c) 2013 The github.com/vmihailenco/msgpack Authors. 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are 16 | met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following disclaimer 22 | in the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | notices: [] 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/vmihailenco/tagparser/v2.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/vmihailenco/tagparser/v2 3 | version: v2.0.0 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/vmihailenco/tagparser/v2 7 | license: bsd-2-clause 8 | licenses: 9 | - sources: LICENSE 10 | text: | 11 | Copyright (c) 2019 The github.com/vmihailenco/tagparser Authors. 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are 16 | met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following disclaimer 22 | in the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | notices: [] 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/vmihailenco/msgpack/v5/msgpcode.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/vmihailenco/msgpack/v5/msgpcode 3 | version: v5.4.1 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/vmihailenco/msgpack/v5/msgpcode 7 | license: bsd-2-clause 8 | licenses: 9 | - sources: v5@v5.4.1/LICENSE 10 | text: | 11 | Copyright (c) 2013 The github.com/vmihailenco/msgpack Authors. 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are 16 | met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following disclaimer 22 | in the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | notices: [] 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/vmihailenco/tagparser/v2/internal.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/vmihailenco/tagparser/v2/internal 3 | version: v2.0.0 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/vmihailenco/tagparser/v2/internal 7 | license: bsd-2-clause 8 | licenses: 9 | - sources: v2@v2.0.0/LICENSE 10 | text: | 11 | Copyright (c) 2019 The github.com/vmihailenco/tagparser Authors. 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are 16 | met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following disclaimer 22 | in the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | notices: [] 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/vmihailenco/tagparser/v2/internal/parser.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/vmihailenco/tagparser/v2/internal/parser 3 | version: v2.0.0 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/github.com/vmihailenco/tagparser/v2/internal/parser 7 | license: bsd-2-clause 8 | licenses: 9 | - sources: v2@v2.0.0/LICENSE 10 | text: | 11 | Copyright (c) 2019 The github.com/vmihailenco/tagparser Authors. 12 | All rights reserved. 13 | 14 | Redistribution and use in source and binary forms, with or without 15 | modification, are permitted provided that the following conditions are 16 | met: 17 | 18 | * Redistributions of source code must retain the above copyright 19 | notice, this list of conditions and the following disclaimer. 20 | * Redistributions in binary form must reproduce the above 21 | copyright notice, this list of conditions and the following disclaimer 22 | in the documentation and/or other materials provided with the 23 | distribution. 24 | 25 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 26 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 27 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 28 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 29 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 30 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 31 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 32 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 33 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 34 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 35 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 36 | notices: [] 37 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/go.bug.st/f.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: go.bug.st/f 3 | version: v0.4.0 4 | type: go 5 | summary: Package f is a golang library implementing some basic algorithms. 6 | homepage: https://pkg.go.dev/go.bug.st/f 7 | license: bsd-3-clause 8 | licenses: 9 | - sources: LICENSE 10 | text: |2+ 11 | 12 | Copyright (c) 2024, Cristian Maglie. 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions 17 | are met: 18 | 19 | 1. Redistributions of source code must retain the above copyright 20 | notice, this list of conditions and the following disclaimer. 21 | 22 | 2. Redistributions in binary form must reproduce the above copyright 23 | notice, this list of conditions and the following disclaimer in 24 | the documentation and/or other materials provided with the 25 | distribution. 26 | 27 | 3. Neither the name of the copyright holder nor the names of its 28 | contributors may be used to endorse or promote products derived 29 | from this software without specific prior written permission. 30 | 31 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 32 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 33 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 34 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 35 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 36 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 37 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 40 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 41 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 42 | POSSIBILITY OF SUCH DAMAGE. 43 | 44 | notices: [] 45 | ... 46 | -------------------------------------------------------------------------------- /examples/ping_server/main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | "net" 22 | "os" 23 | 24 | "github.com/arduino/arduino-router/msgpackrpc" 25 | ) 26 | 27 | func main() { 28 | routerAddr := ":8900" 29 | s, err := net.Dial("tcp", routerAddr) 30 | if err != nil { 31 | slog.Error("Failed to connect to router", "addr", routerAddr, "err", err) 32 | os.Exit(1) 33 | } 34 | slog.Info("Connected to router", "addr", routerAddr) 35 | defer s.Close() 36 | 37 | conn := msgpackrpc.NewConnection(s, s, 38 | func(ctx context.Context, _ msgpackrpc.FunctionLogger, method string, params []any) (_result any, _err any) { 39 | slog.Info("Received request", "method", method, "params", params) 40 | if method == "ping" { 41 | return params, nil 42 | } 43 | return nil, "method not found: " + method 44 | }, 45 | nil, 46 | nil, 47 | ) 48 | defer conn.Close() 49 | go conn.Run() 50 | 51 | // Register the ping method 52 | ctx := context.Background() 53 | _, reqErr, err := conn.SendRequest(ctx, "$/register", "ping") 54 | if err != nil { 55 | slog.Error("Failed to send register request for ping method", "err", reqErr) 56 | return 57 | } 58 | if reqErr != nil { 59 | slog.Error("Failed to register ping method", "err", reqErr) 60 | return 61 | } 62 | 63 | // Wait forever 64 | select {} 65 | } 66 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/spf13/pflag.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/spf13/pflag 3 | version: v1.0.9 4 | type: go 5 | summary: Package pflag is a drop-in replacement for Go's flag package, implementing 6 | POSIX/GNU-style --flags. 7 | homepage: https://pkg.go.dev/github.com/spf13/pflag 8 | license: bsd-3-clause 9 | licenses: 10 | - sources: LICENSE 11 | text: | 12 | Copyright (c) 2012 Alex Ogier. All rights reserved. 13 | Copyright (c) 2012 The Go Authors. All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions are 17 | met: 18 | 19 | * Redistributions of source code must retain the above copyright 20 | notice, this list of conditions and the following disclaimer. 21 | * Redistributions in binary form must reproduce the above 22 | copyright notice, this list of conditions and the following disclaimer 23 | in the documentation and/or other materials provided with the 24 | distribution. 25 | * Neither the name of Google Inc. nor the names of its 26 | contributors may be used to endorse or promote products derived from 27 | this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 30 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 31 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 32 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 33 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 34 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 35 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 36 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 37 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 38 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 39 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 40 | notices: [] 41 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | The examples here shows how an RPC communication should work. 2 | 3 | - `ping_server` is a MsgPack RPC server that answers to the `ping` method. It connects to the MsgPack RPC Router and registers the `"ping"` method, so other client can use it. 4 | - `ping_client` is a MsgPack RPC client that sends a `ping` request and prints the response. It connects to the MsgPack RPC Router to send the request. 5 | - `msgpackdump` is a MsgPack debug tool. It reads a MsgPack stream from the file path given as argument and prints the decoded stream to stdout as ASCII strings. 6 | 7 | To test the examples above, for the current directory: 8 | 9 | 1. Open a terminal window and run the MsgPack RPC Router: 10 | ``` 11 | $ go run ../main.go 12 | 2025/04/30 16:09:10 INFO Listening for TCP/IP services listen_addr=:8900 13 | ``` 14 | 2. Open another terminal window and run `ping_server`: 15 | ``` 16 | $ go run ping_server/main.go 17 | 2025/04/30 16:11:17 INFO Connected to router addr=:8900 18 | ``` 19 | 3. Open another terminal window and run `ping_client`: 20 | ``` 21 | $ go run ping_client/main.go 22 | [HELLO 1 true 5] 23 | ``` 24 | The array `[HELLO 1 true 5]` is the response the the `ping` RPC request. 25 | 4. Kill the `ping_server` in the second terminal and retry `ping_client`: 26 | ``` 27 | $ go run ping_client/main.go 28 | method ping not available 29 | ``` 30 | This time the server is not running and the registered `ping` method is no longer available. 31 | 32 | Let's see now how `msgpackdump` can be used: 33 | 34 | 5. In this example we will show how to decode a test file called `array` that contains the array `[32 nil false "HELLO"]` encoded in MsgPack format: 35 | ``` 36 | $ hexdump msgpackdump/testdata/array -C 37 | 00000000 94 20 c0 c2 a6 48 45 4c 4c 4f 21 |. ...HELLO!| 38 | 0000000b 39 | $ go run msgpackdump/main.go msgpackdump/testdata/array 40 | ([]interface {}) (len=4 cap=4) { 41 | (int8) 32, 42 | (interface {}) , 43 | (bool) false, 44 | (string) (len=6) "HELLO!" 45 | } 46 | 2025/04/30 16:29:11 EOF 47 | exit status 1 48 | ``` 49 | -------------------------------------------------------------------------------- /debian/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.25.1 AS go 2 | 3 | ARG BINARY_NAME 4 | RUN test -n "${BINARY_NAME}" || (echo "Error: BINARY_NAME is not set" && exit 1) 5 | ARG VERSION 6 | RUN test -n "${VERSION}" || (echo "Error: VERSION is not set" && exit 1) 7 | ARG ARCH 8 | RUN test -n "${ARCH}" || (echo "Error: ARCH is not set" && exit 1) 9 | ARG RELEASE 10 | 11 | # Download dependencies first (this layer will be cached by docker for quicker image builds). 12 | # This will be recomputed only when these two files change, instead of every build. 13 | COPY go.mod go.sum* /app/ 14 | WORKDIR /app 15 | 16 | RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ 17 | go mod download 18 | 19 | ADD . . 20 | 21 | RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \ 22 | --mount=type=cache,target=/root/.cache/go-build,sharing=locked \ 23 | export VERSION=$(echo "${VERSION}" | sed -e "s/^v\(.*\)/\1/") \ 24 | export LDFLAGS=$([ -n "$RELEASE" ] && echo "-s -w" || echo "") \ 25 | && GOOS=linux GOARCH=${ARCH} go build -ldflags "${LDFLAGS} -X 'main.Version=${VERSION}'" -o ${BINARY_NAME} . 26 | 27 | FROM debian:bookworm AS debian 28 | 29 | ARG VERSION 30 | RUN test -n "${VERSION}" || (echo "Error: VERSION is not set" && exit 1) 31 | ARG REVISION="1" 32 | ARG ARCH 33 | RUN test -n "${ARCH}" || (echo "Error: ARCH is not set" && exit 1) 34 | ARG DEB_NAME 35 | RUN test -n "${DEB_NAME}" || (echo "Error: DEB_NAME is not set" && exit 1) 36 | ARG BINARY_NAME 37 | RUN test -n "${BINARY_NAME}" || (echo "Error: BINARY_NAME is not set" && exit 1) 38 | 39 | RUN apt-get update && apt-get install -y sed 40 | 41 | COPY ./debian/${DEB_NAME} /${DEB_NAME}/ 42 | COPY --from=go /app/${BINARY_NAME} /${DEB_NAME}/usr/bin/${BINARY_NAME} 43 | 44 | # Go application are tagged with `v` prefix, this remove the first v if present 45 | RUN export VERSION=$(echo "${VERSION}" | sed -e "s/^v\(.*\)/\1/") && \ 46 | sed -i "s/\$ARCH/${ARCH}/" /${DEB_NAME}/DEBIAN/control && \ 47 | sed -i "s/\$VERSION/${VERSION}/" /${DEB_NAME}/DEBIAN/control && \ 48 | dpkg-deb --build --root-owner-group /${DEB_NAME} &&\ 49 | mv /${DEB_NAME}.deb "/${DEB_NAME}_${VERSION}-${REVISION}_${ARCH}.deb" 50 | 51 | FROM scratch 52 | 53 | COPY --from=debian /*.deb / 54 | -------------------------------------------------------------------------------- /examples/generic_sock_client/main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "net" 22 | "os" 23 | "strconv" 24 | 25 | "github.com/arduino/arduino-router/msgpackrpc" 26 | 27 | "github.com/arduino/go-paths-helper" 28 | ) 29 | 30 | func main() { 31 | if len(os.Args) < 2 { 32 | fmt.Printf("Usage: %s [ [ ...]]\n", os.Args[0]) 33 | os.Exit(1) 34 | } 35 | 36 | c, err := net.Dial("unix", paths.TempDir().Join("arduino-router.sock").String()) 37 | if err != nil { 38 | fmt.Println("Error connecting to server:", err) 39 | os.Exit(1) 40 | } 41 | 42 | conn := msgpackrpc.NewConnection(c, c, nil, nil, nil) 43 | defer conn.Close() 44 | go conn.Run() 45 | 46 | // Client 47 | method := os.Args[1] 48 | args := []any{} 49 | for _, arg := range os.Args[2:] { 50 | if arg == "true" { 51 | args = append(args, true) 52 | } else if arg == "false" { 53 | args = append(args, false) 54 | } else if arg == "nil" { 55 | args = append(args, nil) 56 | } else if i, err := strconv.Atoi(arg); err == nil { 57 | args = append(args, i) 58 | } else { 59 | args = append(args, arg) 60 | } 61 | } 62 | reqResult, reqError, err := conn.SendRequest(context.Background(), method, args...) 63 | if err != nil { 64 | fmt.Println("Error sending request:", err) 65 | return 66 | } 67 | if reqError != nil { 68 | fmt.Println("Error in response:", reqError) 69 | } else { 70 | fmt.Println("Response:", reqResult) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /msgpackrpc/README.md: -------------------------------------------------------------------------------- 1 | # A MessagePack RPC client/server implementation. 2 | 3 | This package implements a MessagePack RPC point-to-point communication. The actors on each side of the communication can perform RPC calls to the other side so, technically, they can act simultaneously as client or server. By the way, for the sake of simplicity, from now on we will refer to them simply with "client". 4 | 5 | The protocol supported is defined here https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md. We have 3 types of messages defined as: 6 | 7 | - REQUEST: this message is an array of 4 elements containing in order: 8 | 1. `type`: Fixed as number `0` (to identify the message as a REQUEST). 9 | 2. `msgid`: A message ID, a 32-bit unsigned integer used as a sequence number to match the response (the server's response to the REQUEST will have the same `msgid`). 10 | 3. `method`: A string containing the called method name. 11 | 4. `params`: An array of the function arguments. 12 | 13 | - RESPONSE: this message is an array of 4 elements containing in order: 14 | 1. `type`: Fixed as number `1` (to identify the message as a RESPONSE). 15 | 2. `msgid`: A message ID. 16 | 3. `error`: The error returned from the method, or `null` if the method was successful. 17 | 4. `result`: The result of the method. It should be `null` if an error occurred. 18 | 19 | - NOTIFICATION: this message is an array of 3 elements containing in order: 20 | 1. `type`: Fixed number `2` (to identify this message as a NOTIFICATION). 21 | 2. `methods`: The method name. 22 | 3. `params`: An array of the function parameters. 23 | 24 | ### RPC Request cancelation support 25 | 26 | The MessagePack RPC protocol implemented in this package provides also a way for a client to cancel a REQUEST. To do so the client must send a NOTIFICATION to the `$/cancel` method with a single parameter matching the `msgid` of the REQUEST to cancel. 27 | 28 | ``` 29 | [2 "$/cancel" [ MSGID ]] 30 | ``` 31 | 32 | The server will send an interrupt to the subroutine handling the original REQUEST to inform that the client is no longer interested in the RESPONSE. The server could return immediately an empty RESPONSE with an "interrupted" error, or it may ignore the cancel notification, in this latter case the cancelation will not produce any visible effect. 33 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/go.bug.st/serial/unixutils.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: go.bug.st/serial/unixutils 3 | version: v1.6.4 4 | type: go 5 | summary: 6 | homepage: https://pkg.go.dev/go.bug.st/serial/unixutils 7 | license: bsd-3-clause 8 | licenses: 9 | - sources: serial@v1.6.4/LICENSE 10 | text: |2+ 11 | 12 | Copyright (c) 2014-2024, Cristian Maglie. 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions 17 | are met: 18 | 19 | 1. Redistributions of source code must retain the above copyright 20 | notice, this list of conditions and the following disclaimer. 21 | 22 | 2. Redistributions in binary form must reproduce the above copyright 23 | notice, this list of conditions and the following disclaimer in 24 | the documentation and/or other materials provided with the 25 | distribution. 26 | 27 | 3. Neither the name of the copyright holder nor the names of its 28 | contributors may be used to endorse or promote products derived 29 | from this software without specific prior written permission. 30 | 31 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 32 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 33 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 34 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 35 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 36 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 37 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 40 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 41 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 42 | POSSIBILITY OF SUCH DAMAGE. 43 | 44 | - sources: serial@v1.6.4/README.md 45 | text: |- 46 | This software is released under the [BSD 3-clause license]. 47 | 48 | [contributors]: https://github.com/bugst/go-serial/graphs/contributors 49 | [BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE 50 | notices: [] 51 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/go.bug.st/serial.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: go.bug.st/serial 3 | version: v1.6.4 4 | type: go 5 | summary: Package serial is a cross-platform serial library for the go language. 6 | homepage: https://pkg.go.dev/go.bug.st/serial 7 | license: bsd-3-clause 8 | licenses: 9 | - sources: LICENSE 10 | text: |2+ 11 | 12 | Copyright (c) 2014-2024, Cristian Maglie. 13 | All rights reserved. 14 | 15 | Redistribution and use in source and binary forms, with or without 16 | modification, are permitted provided that the following conditions 17 | are met: 18 | 19 | 1. Redistributions of source code must retain the above copyright 20 | notice, this list of conditions and the following disclaimer. 21 | 22 | 2. Redistributions in binary form must reproduce the above copyright 23 | notice, this list of conditions and the following disclaimer in 24 | the documentation and/or other materials provided with the 25 | distribution. 26 | 27 | 3. Neither the name of the copyright holder nor the names of its 28 | contributors may be used to endorse or promote products derived 29 | from this software without specific prior written permission. 30 | 31 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 32 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 33 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 34 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 35 | COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 36 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 37 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 38 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 39 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 40 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 41 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 42 | POSSIBILITY OF SUCH DAMAGE. 43 | 44 | - sources: README.md 45 | text: |- 46 | This software is released under the [BSD 3-clause license]. 47 | 48 | [contributors]: https://github.com/bugst/go-serial/graphs/contributors 49 | [BSD 3-clause license]: https://github.com/bugst/go-serial/blob/master/LICENSE 50 | notices: [] 51 | -------------------------------------------------------------------------------- /examples/mult_server/main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "context" 20 | "log/slog" 21 | "net" 22 | "os" 23 | 24 | "github.com/arduino/arduino-router/msgpackrpc" 25 | ) 26 | 27 | func main() { 28 | routerAddr := ":8900" 29 | s, err := net.Dial("tcp", routerAddr) 30 | if err != nil { 31 | slog.Error("Failed to connect to router", "addr", routerAddr, "err", err) 32 | os.Exit(1) 33 | } 34 | slog.Info("Connected to router", "addr", routerAddr) 35 | defer s.Close() 36 | 37 | conn := msgpackrpc.NewConnection(s, s, 38 | func(ctx context.Context, _ msgpackrpc.FunctionLogger, method string, params []any) (_result any, _err any) { 39 | slog.Info("Received request", "method", method, "params", params) 40 | if method == "mult" { 41 | if len(params) != 2 { 42 | return nil, "invalid params" 43 | } 44 | a, ok := params[0].(float64) 45 | if !ok { 46 | return nil, "invalid param type, expected float32" 47 | } 48 | b, ok := params[1].(float64) 49 | if !ok { 50 | return nil, "invalid param type, expected float32" 51 | } 52 | return a * b, nil 53 | } 54 | return nil, "method not found: " + method 55 | }, 56 | nil, 57 | nil, 58 | ) 59 | defer conn.Close() 60 | go conn.Run() 61 | 62 | // Register the ping method 63 | ctx := context.Background() 64 | _, reqErr, err := conn.SendRequest(ctx, "$/register", "mult") 65 | if err != nil { 66 | slog.Error("Failed to send register request for ping method", "err", err) 67 | return 68 | } 69 | if reqErr != nil { 70 | slog.Error("Failed to register ping method", "err", reqErr) 71 | return 72 | } 73 | 74 | // Wait forever 75 | select {} 76 | } 77 | -------------------------------------------------------------------------------- /msgpackrpc/type_conversion.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrpc 17 | 18 | import "math" 19 | 20 | // ToInt converts a value of any type to an int. It returns the converted int and a boolean indicating success. 21 | func ToInt(value any) (int, bool) { 22 | switch v := value.(type) { 23 | case int: 24 | return v, true 25 | case int8: 26 | return int(v), true 27 | case int16: 28 | return int(v), true 29 | case int32: 30 | return int(v), true 31 | case int64: 32 | return int(v), true 33 | case uint: 34 | if v > math.MaxInt64 { 35 | return 0, false 36 | } 37 | return int(v), true 38 | case uint8: 39 | return int(v), true 40 | case uint16: 41 | return int(v), true 42 | case uint32: 43 | return int(v), true 44 | case uint64: 45 | if v > math.MaxInt64 { 46 | return 0, false 47 | } 48 | return int(v), true 49 | default: 50 | return 0, false 51 | } 52 | } 53 | 54 | // ToUint converts a value of any type to an uint. It returns the converted int and a boolean indicating success. 55 | func ToUint(value any) (uint, bool) { 56 | switch v := value.(type) { 57 | case int: 58 | if v < 0 { 59 | return 0, false 60 | } 61 | return uint(v), true 62 | case int8: 63 | if v < 0 { 64 | return 0, false 65 | } 66 | return uint(v), true 67 | case int16: 68 | if v < 0 { 69 | return 0, false 70 | } 71 | return uint(v), true 72 | case int32: 73 | if v < 0 { 74 | return 0, false 75 | } 76 | return uint(v), true 77 | case int64: 78 | if v < 0 { 79 | return 0, false 80 | } 81 | return uint(v), true 82 | case uint: 83 | return v, true 84 | case uint8: 85 | return uint(v), true 86 | case uint16: 87 | return uint(v), true 88 | case uint32: 89 | return uint(v), true 90 | case uint64: 91 | return uint(v), true 92 | default: 93 | return 0, false 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /.licensed.yml: -------------------------------------------------------------------------------- 1 | sources: 2 | go: true 3 | 4 | cache_path: .licenses/ 5 | 6 | apps: 7 | - source_path: ./ 8 | 9 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-dependencies/GPL-3.0/.licensed.yml 10 | allowed: 11 | # The following are based on: https://www.gnu.org/licenses/license-list.html#GPLCompatibleLicenses 12 | - gpl-1.0-or-later 13 | - gpl-1.0+ # Deprecated ID for `gpl-1.0-or-later` 14 | - gpl-2.0-or-later 15 | - gpl-2.0+ # Deprecated ID for `gpl-2.0-or-later` 16 | - gpl-3.0-only 17 | - gpl-3.0 # Deprecated ID for `gpl-3.0-only` 18 | - gpl-3.0-or-later 19 | - gpl-3.0+ # Deprecated ID for `gpl-3.0-or-later` 20 | - lgpl-2.0-or-later 21 | - lgpl-2.0+ # Deprecated ID for `lgpl-2.0-or-later` 22 | - lgpl-2.1-only 23 | - lgpl-2.1 # Deprecated ID for `lgpl-2.1-only` 24 | - lgpl-2.1-or-later 25 | - lgpl-2.1+ # Deprecated ID for `lgpl-2.1-or-later` 26 | - lgpl-3.0-only 27 | - lgpl-3.0 # Deprecated ID for `lgpl-3.0-only` 28 | - lgpl-3.0-or-later 29 | - lgpl-3.0+ # Deprecated ID for `lgpl-3.0-or-later` 30 | - fsfap 31 | - apache-2.0 32 | - artistic-2.0 33 | - clartistic 34 | - sleepycat 35 | - bsl-1.0 36 | - bsd-3-clause 37 | - cecill-2.0 38 | - bsd-3-clause-clear 39 | # "Cryptix General License" - no SPDX ID (https://github.com/spdx/license-list-XML/issues/456) 40 | - ecos-2.0 41 | - ecl-2.0 42 | - efl-2.0 43 | - eudatagrid 44 | - mit 45 | - bsd-2-clause # Subsumed by `bsd-2-clause-views` 46 | - bsd-2-clause-netbsd # Deprecated ID for `bsd-2-clause` 47 | - bsd-2-clause-views # This is the version linked from https://www.gnu.org/licenses/license-list.html#FreeBSD 48 | - bsd-2-clause-freebsd # Deprecated ID for `bsd-2-clause-views` 49 | - ftl 50 | - hpnd 51 | - imatix 52 | - imlib2 53 | - ijg 54 | # "Informal license" - this is a general class of license 55 | - intel 56 | - isc 57 | - mpl-2.0 58 | - ncsa 59 | # "License of Netscape JavaScript" - no SPDX ID 60 | - oldap-2.7 61 | # "License of Perl 5 and below" - possibly `Artistic-1.0-Perl` ? 62 | - cc0-1.0 63 | - cc-pddc 64 | - psf-2.0 65 | - ruby 66 | - sgi-b-2.0 67 | - smlnj 68 | - standardml-nj # Deprecated ID for `smlnj` 69 | - unicode-dfs-2015 70 | - upl-1.0 71 | - unlicense 72 | - vim 73 | - w3c 74 | - wtfpl 75 | - lgpl-2.0-or-later with wxwindows-exception-3.1 76 | - wxwindows # Deprecated ID for `lgpl-2.0-or-later with wxwindows-exception-3.1` 77 | - x11 78 | - xfree86-1.1 79 | - zlib 80 | - zpl-2.0 81 | - zpl-2.1 82 | # The following are based on individual license text 83 | - eupl-1.2 84 | - liliq-r-1.1 85 | - liliq-rplus-1.1 86 | -------------------------------------------------------------------------------- /msgpackrpc/logger.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrpc 17 | 18 | import ( 19 | "time" 20 | ) 21 | 22 | type Logger interface { 23 | LogOutgoingRequest(id MessageID, method string, params []any) 24 | LogIncomingRequest(id MessageID, method string, params []any) FunctionLogger 25 | LogOutgoingResponse(id MessageID, method string, resp any, respErr any) 26 | LogIncomingResponse(id MessageID, method string, resp any, respErr any) 27 | LogOutgoingNotification(method string, params []any) 28 | LogIncomingNotification(method string, params []any) FunctionLogger 29 | LogIncomingCancelRequest(id MessageID) 30 | LogOutgoingCancelRequest(id MessageID) 31 | LogIncomingDataDelay(time.Duration) 32 | LogOutgoingDataDelay(time.Duration) 33 | } 34 | 35 | type FunctionLogger interface { 36 | Logf(format string, a ...interface{}) 37 | } 38 | 39 | type NullLogger struct{} 40 | 41 | func (NullLogger) LogOutgoingRequest(id MessageID, method string, params []any) { 42 | } 43 | 44 | func (NullLogger) LogIncomingRequest(id MessageID, method string, params []any) FunctionLogger { 45 | return &NullFunctionLogger{} 46 | } 47 | 48 | func (NullLogger) LogOutgoingResponse(id MessageID, method string, resp any, respErr any) { 49 | } 50 | 51 | func (NullLogger) LogIncomingResponse(id MessageID, method string, resp any, respErr any) { 52 | } 53 | 54 | func (NullLogger) LogOutgoingNotification(method string, params []any) { 55 | } 56 | 57 | func (NullLogger) LogIncomingNotification(method string, params []any) FunctionLogger { 58 | return &NullFunctionLogger{} 59 | } 60 | 61 | func (NullLogger) LogIncomingCancelRequest(id MessageID) {} 62 | 63 | func (NullLogger) LogOutgoingCancelRequest(id MessageID) {} 64 | 65 | type NullFunctionLogger struct{} 66 | 67 | func (NullFunctionLogger) Logf(format string, a ...interface{}) {} 68 | 69 | func (NullLogger) LogIncomingDataDelay(time.Duration) {} 70 | 71 | func (NullLogger) LogOutgoingDataDelay(time.Duration) {} 72 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/arduino/arduino-router 2 | 3 | go 1.25 4 | 5 | require ( 6 | github.com/arduino/go-paths-helper v1.14.0 7 | github.com/davecgh/go-spew v1.1.1 8 | github.com/djherbis/buffer v1.2.0 9 | github.com/djherbis/nio/v3 v3.0.1 10 | github.com/spf13/cobra v1.10.1 11 | github.com/stretchr/testify v1.11.1 12 | github.com/vmihailenco/msgpack/v5 v5.4.1 13 | go.bug.st/f v0.4.0 14 | go.bug.st/serial v1.6.4 15 | golang.org/x/sys v0.37.0 16 | ) 17 | 18 | require ( 19 | dario.cat/mergo v1.0.0 // indirect 20 | github.com/Ladicle/tabwriter v1.0.0 // indirect 21 | github.com/Masterminds/semver/v3 v3.4.0 // indirect 22 | github.com/Microsoft/go-winio v0.6.2 // indirect 23 | github.com/ProtonMail/go-crypto v1.1.6 // indirect 24 | github.com/alecthomas/chroma/v2 v2.19.0 // indirect 25 | github.com/chainguard-dev/git-urls v1.0.2 // indirect 26 | github.com/cloudflare/circl v1.6.1 // indirect 27 | github.com/creack/goselect v0.1.2 // indirect 28 | github.com/cyphar/filepath-securejoin v0.4.1 // indirect 29 | github.com/dlclark/regexp2 v1.11.5 // indirect 30 | github.com/dominikbraun/graph v0.23.0 // indirect 31 | github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect 32 | github.com/emirpasic/gods v1.18.1 // indirect 33 | github.com/fatih/color v1.18.0 // indirect 34 | github.com/fsnotify/fsnotify v1.9.0 // indirect 35 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 36 | github.com/go-git/go-billy/v5 v5.6.2 // indirect 37 | github.com/go-git/go-git/v5 v5.16.2 // indirect 38 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 39 | github.com/go-task/task/v3 v3.44.1 // indirect 40 | github.com/go-task/template v0.2.0 // indirect 41 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect 42 | github.com/google/uuid v1.6.0 // indirect 43 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 44 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 45 | github.com/joho/godotenv v1.5.1 // indirect 46 | github.com/kevinburke/ssh_config v1.2.0 // indirect 47 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 48 | github.com/mattn/go-colorable v0.1.13 // indirect 49 | github.com/mattn/go-isatty v0.0.20 // indirect 50 | github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect 51 | github.com/pjbgf/sha1cd v0.3.2 // indirect 52 | github.com/pmezard/go-difflib v1.0.0 // indirect 53 | github.com/puzpuzpuz/xsync/v3 v3.5.1 // indirect 54 | github.com/sajari/fuzzy v1.0.0 // indirect 55 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect 56 | github.com/skeema/knownhosts v1.3.1 // indirect 57 | github.com/spf13/pflag v1.0.9 // indirect 58 | github.com/stretchr/objx v0.5.2 // indirect 59 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 60 | github.com/xanzy/ssh-agent v0.3.3 // indirect 61 | github.com/zeebo/xxh3 v1.0.2 // indirect 62 | golang.org/x/crypto v0.37.0 // indirect 63 | golang.org/x/net v0.39.0 // indirect 64 | golang.org/x/sync v0.16.0 // indirect 65 | golang.org/x/term v0.33.0 // indirect 66 | gopkg.in/warnings.v0 v0.1.2 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | mvdan.cc/sh/v3 v3.12.0 // indirect 69 | ) 70 | 71 | tool github.com/go-task/task/v3/cmd/task 72 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/golang.org/x/sys/unix.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: golang.org/x/sys/unix 3 | version: v0.37.0 4 | type: go 5 | summary: Package unix contains an interface to the low-level operating system primitives. 6 | homepage: https://pkg.go.dev/golang.org/x/sys/unix 7 | license: bsd-3-clause 8 | licenses: 9 | - sources: sys@v0.37.0/LICENSE 10 | text: | 11 | Copyright 2009 The Go Authors. 12 | 13 | Redistribution and use in source and binary forms, with or without 14 | modification, are permitted provided that the following conditions are 15 | met: 16 | 17 | * Redistributions of source code must retain the above copyright 18 | notice, this list of conditions and the following disclaimer. 19 | * Redistributions in binary form must reproduce the above 20 | copyright notice, this list of conditions and the following disclaimer 21 | in the documentation and/or other materials provided with the 22 | distribution. 23 | * Neither the name of Google LLC nor the names of its 24 | contributors may be used to endorse or promote products derived from 25 | this software without specific prior written permission. 26 | 27 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 28 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 29 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 30 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 31 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 32 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 33 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 34 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 35 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 36 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 37 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 38 | - sources: sys@v0.37.0/PATENTS 39 | text: | 40 | Additional IP Rights Grant (Patents) 41 | 42 | "This implementation" means the copyrightable works distributed by 43 | Google as part of the Go project. 44 | 45 | Google hereby grants to You a perpetual, worldwide, non-exclusive, 46 | no-charge, royalty-free, irrevocable (except as stated in this section) 47 | patent license to make, have made, use, offer to sell, sell, import, 48 | transfer and otherwise run, modify and propagate the contents of this 49 | implementation of Go, where such license applies only to those patent 50 | claims, both currently owned or controlled by Google and acquired in 51 | the future, licensable by Google that are necessarily infringed by this 52 | implementation of Go. This grant does not include claims that would be 53 | infringed only as a consequence of further modification of this 54 | implementation. If you or your agent or exclusive licensee institute or 55 | order or agree to the institution of patent litigation against any 56 | entity (including a cross-claim or counterclaim in a lawsuit) alleging 57 | that this implementation of Go or any code incorporated within this 58 | implementation of Go constitutes direct or contributory patent 59 | infringement, or inducement of patent infringement, then any patent 60 | rights granted to you under this License for this implementation of Go 61 | shall terminate as of the date such litigation is filed. 62 | notices: [] 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `arduino-router` is a MessagePack RPC Router 2 | 3 | This module implements a MessagePack RPC Router that allows RPC calls between multiple MessagePack RPC clients, connected together in a star topology network, where the Router is the central node. 4 | 5 | Each client can connect to the Router and expose RPC services by registering its methods using a special RPC call implemented in the Router. During normal operation, when the Router receives an RPC request, it redirects the request to the client that has previously registered the corresponding method and it will forward back the response to the client that originated the RPC request. 6 | 7 | To understand more about MessagePack encoding see: 8 | 9 | This module provides also a MessagePack RPC client in the `msgpackrpc` package. To get more details about MessagePack RPC and this implementation see [here](msgpackrpc/README.md). 10 | 11 | ### Methods implemented in the Router 12 | 13 | The Router implements a single `$/register` method that is used by a client to register the RPC calls it wants to expose. A single string parameter is required in the call: the method name to register. 14 | 15 | | Client P <-> Router | 16 | | ------------------------------------------------------------------- | 17 | | `[REQUEST, 50, "$/register", ["ping"]]` >> | 18 | | Method successfully registered:
`[RESPONSE, 50, null, true]` << | 19 | | Error:
`[RESPONSE, 50, "route already exists: ping", null]` << | 20 | 21 | After the method is registered another client may perform an RPC request to that method, the Router will take care to forward the messages back and forth. A typical RPC call example may be: 22 | 23 | | Client A <-> Router | Router <-> Client P | 24 | | -------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | 25 | | Client A does an RPC call to the Router
`[REQUEST, 32, "ping", [1, true]]` >> | | 26 | | | Router forwards the request to Client P
>> `[REQUEST, 51, "ping", [1, true]]` | 27 | | | Client P process the request and replies
<< `[RESPONSE, 51, null, [1, true]]` | 28 | | The Router forwards back the response
`[RESPONSE, 32, null, [1, true]]` << | | 29 | 30 | Note that the request ID has been remapped by the Router: it keeps track of all active requests so the message IDs will not conflict between different clients. 31 | 32 | ### Calling an unregistered method 33 | 34 | A request to a non-registered method will result in an error: 35 | 36 | | Client A <-> Router | 37 | | ----------------------------------------------------------------------------------------------------------- | 38 | | Client A does an RPC call to the Router
`[REQUEST, 33, "xxxx", [1, true]]` >> | 39 | | The Router didn't know how to handle the request
`[RESPONSE, 33, "method xxxx not available", null]` << | 40 | 41 | ### Unregistering methods (via `$/reset` method call) 42 | 43 | A client can drop all its registered methods by calling the `$/reset` method, with an empty parameter list. 44 | 45 | | Client A <-> Router | 46 | | ------------------------------------------------------------------------------------- | 47 | | Clian A request to remove all registered methods
`[REQUEST, 52, "$/reset", []]` >> | 48 | | The Router should always succeed
`[RESPONSE, 52, null, true]` << | 49 | 50 | ### Unregistering methods (via client disconnection) 51 | 52 | When a client disconnects all the registered methods from that client are dropped. 53 | 54 | ### Router serial connection 55 | 56 | The MsgPack RPC Router can establish a physical connection with a serial port. This connection can register and call RPC methods as any other network TCP/IP connection. The serial port address is specified via the command line flag `-p PORT`, if this flag is set the Router will try to open the serial port at startup. 57 | 58 | If the serial port fails for some reason, the router will retry to connect automatically after 5 seconds. 59 | 60 | The Router has a RPC methods to "open" and "close" the serial connection on request: 61 | 62 | - The `$/serial/open` method will open the serial port connection. This method returns immediately. 63 | - The `$/serial/close` method will close the serial port connection. This method returns only after the port has been successfully disconnected. 64 | -------------------------------------------------------------------------------- /.github/workflows/check-go-dependencies-task.yml: -------------------------------------------------------------------------------- 1 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-go-dependencies-task.md 2 | name: Check Go Dependencies 3 | 4 | # See: https://docs.github.com/actions/reference/workflows-and-actions/events-that-trigger-workflows 5 | on: 6 | create: 7 | push: 8 | paths: 9 | - ".github/workflows/check-go-dependencies-task.ya?ml" 10 | - ".licenses/**" 11 | - ".licensed.json" 12 | - ".licensed.ya?ml" 13 | - "Taskfile.ya?ml" 14 | - "**/.gitmodules" 15 | - "**/go.mod" 16 | - "**/go.sum" 17 | pull_request: 18 | paths: 19 | - ".github/workflows/check-go-dependencies-task.ya?ml" 20 | - ".licenses/**" 21 | - ".licensed.json" 22 | - ".licensed.ya?ml" 23 | - "Taskfile.ya?ml" 24 | - "**/.gitmodules" 25 | - "**/go.mod" 26 | - "**/go.sum" 27 | schedule: 28 | # Run periodically to catch breakage caused by external changes. 29 | - cron: "0 8 * * WED" 30 | workflow_dispatch: 31 | repository_dispatch: 32 | 33 | jobs: 34 | run-determination: 35 | runs-on: ubuntu-latest 36 | permissions: {} 37 | outputs: 38 | result: ${{ steps.determination.outputs.result }} 39 | steps: 40 | - name: Determine if the rest of the workflow should run 41 | id: determination 42 | run: | 43 | RELEASE_BRANCH_REGEX="refs/heads/[0-9]+.[0-9]+.x" 44 | # The `create` event trigger doesn't support `branches` filters, so it's necessary to use Bash instead. 45 | if [[ 46 | "${{ github.event_name }}" != "create" || 47 | "${{ github.ref }}" =~ $RELEASE_BRANCH_REGEX 48 | ]]; then 49 | # Run the other jobs. 50 | RESULT="true" 51 | else 52 | # There is no need to run the other jobs. 53 | RESULT="false" 54 | fi 55 | 56 | echo "result=$RESULT" >>$GITHUB_OUTPUT 57 | 58 | check-cache: 59 | needs: run-determination 60 | if: needs.run-determination.outputs.result == 'true' 61 | runs-on: ubuntu-latest 62 | permissions: 63 | contents: read 64 | 65 | steps: 66 | - name: Checkout repository 67 | uses: actions/checkout@v5 68 | with: 69 | submodules: recursive 70 | 71 | # This is required to allow licensee/setup-licensed to install Licensed via Ruby gem. 72 | - name: Install Ruby 73 | uses: ruby/setup-ruby@v1 74 | with: 75 | ruby-version: ruby # Install latest version 76 | 77 | - name: Install licensed 78 | uses: licensee/setup-licensed@v1.3.2 79 | with: 80 | github_token: ${{ secrets.GITHUB_TOKEN }} 81 | version: 5.x 82 | 83 | - name: Install Go 84 | uses: actions/setup-go@v6 85 | with: 86 | go-version-file: go.mod 87 | 88 | - name: Update dependencies license metadata cache 89 | run: | 90 | go tool \ 91 | github.com/go-task/task/v3/cmd/task \ 92 | --silent \ 93 | general:cache-dep-licenses 94 | 95 | - name: Check for outdated cache 96 | id: diff 97 | run: | 98 | git add . 99 | if 100 | ! git diff \ 101 | --cached \ 102 | --color \ 103 | --exit-code 104 | then 105 | echo 106 | echo "::error::Dependency license metadata out of sync. See: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/check-go-dependencies-task.md#metadata-cache" 107 | exit 1 108 | fi 109 | 110 | # Some might find it convenient to have CI generate the cache rather than setting up for it locally 111 | - name: Upload cache to workflow artifact 112 | if: failure() && steps.diff.outcome == 'failure' 113 | uses: actions/upload-artifact@v4 114 | with: 115 | if-no-files-found: error 116 | include-hidden-files: true 117 | name: dep-licenses-cache 118 | path: .licenses/ 119 | 120 | check-deps: 121 | needs: run-determination 122 | if: needs.run-determination.outputs.result == 'true' 123 | runs-on: ubuntu-latest 124 | permissions: 125 | contents: read 126 | 127 | steps: 128 | - name: Checkout repository 129 | uses: actions/checkout@v5 130 | with: 131 | submodules: recursive 132 | 133 | # This is required to allow licensee/setup-licensed to install Licensed via Ruby gem. 134 | - name: Install Ruby 135 | uses: ruby/setup-ruby@v1 136 | with: 137 | ruby-version: ruby # Install latest version 138 | 139 | - name: Install licensed 140 | uses: licensee/setup-licensed@v1.3.2 141 | with: 142 | github_token: ${{ secrets.GITHUB_TOKEN }} 143 | version: 5.x 144 | 145 | - name: Install Go 146 | uses: actions/setup-go@v6 147 | with: 148 | go-version-file: go.mod 149 | 150 | - name: Check for dependencies with unapproved licenses 151 | run: | 152 | go tool \ 153 | github.com/go-task/task/v3/cmd/task \ 154 | --silent \ 155 | general:check-dep-licenses 156 | -------------------------------------------------------------------------------- /Taskfile.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | vars: 3 | GOLANGCI_LINT_VERSION: v2.5.0 4 | GOIMPORTS_VERSION: v0.38.0 5 | DPRINT_VERSION: 0.50.2 6 | VERSION: # if version is not passed we hack the semver by encoding the commit as pre-release 7 | sh: echo "${VERSION:-0.0.0-$(git rev-parse --short HEAD)}" 8 | 9 | tasks: 10 | init: 11 | desc: Setup local env 12 | deps: 13 | - install:linter 14 | - install:goimports 15 | - install:dprint 16 | 17 | install:linter: 18 | cmds: 19 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b .bin/ {{ .GOLANGCI_LINT_VERSION }} 20 | 21 | install:goimports: 22 | cmds: 23 | - go install golang.org/x/tools/cmd/goimports@{{ .GOIMPORTS_VERSION }} 24 | 25 | install:dprint: 26 | cmds: 27 | - curl -fsSL https://dprint.dev/install.sh | sh -s {{ .DPRINT_VERSION }} 28 | - mkdir -p .bin && cp $HOME/.dprint/bin/dprint .bin/dprint # workaround for local install 29 | 30 | test: 31 | desc: Run all go tests 32 | cmds: 33 | - go test ./... -v -race {{ .CLI_ARGS }} 34 | 35 | test:cover: 36 | desc: Run all tests and open cover html report 37 | cmds: 38 | - task test -- -coverprofile=coverage.out 39 | - go tool cover -func=coverage.out 40 | - go tool cover -html=coverage.out 41 | 42 | lint: 43 | desc: Run the linters 44 | cmds: 45 | - ${PWD}/.bin/golangci-lint run --fix -v --timeout 300s {{ .CLI_ARGS }} 46 | 47 | fmt: 48 | desc: Run format 49 | cmds: 50 | - goimports -l -w . 51 | - ${PWD}/.bin/dprint fmt 52 | 53 | fmt-check: 54 | desc: Check format 55 | cmds: 56 | - ${PWD}/.bin/dprint check 57 | 58 | build-deb: 59 | desc: Build debian package 60 | cmds: 61 | - docker build --build-arg BINARY_NAME=arduino-router --build-arg DEB_NAME=arduino-router --build-arg VERSION={{ .VERSION }} --build-arg ARCH={{ .ARCH }} --build-arg RELEASE={{ .RELEASE }} --output=./build -f debian/Dockerfile . 62 | vars: 63 | ARCH: '{{.ARCH | default "arm64"}}' 64 | 65 | release: 66 | desc: Create a tag on the current commit and push it to the remote to create the release 67 | cmds: 68 | - echo "Create tag version {{ .TAG }}" 69 | - git tag "{{ .TAG }}" 70 | - git push origin "{{ .TAG }}" 71 | vars: 72 | TAG: 73 | sh: | 74 | TAGARG="{{.CLI_ARGS}}" 75 | if ! echo "$TAGARG" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+.*$'; then 76 | echo "Error: Version must match pattern 'v[0-9]+.[0-9]+.[0-9]+*'" 77 | echo " Invalid version: $TAGARG" 78 | echo " Valid examples: v1.0.0, v2.1.3, v1.0.0-beta, v3.2.1-rc.1" 79 | exit 1 80 | fi 81 | echo $TAGARG 82 | board:install: 83 | desc: Install arduino-router on the board 84 | interactive: true 85 | cmds: 86 | - rm ./build/*.deb || true 87 | - task: build-deb 88 | - adb shell rm /tmp/*.deb || true 89 | - adb push ./build/arduino-router_*_arm64.deb /tmp/ 90 | - ./scripts/run-remote-sudo.sh "dpkg -i /tmp/arduino-router_*_arm64.deb && needrestart -r a" 91 | 92 | update-deb-copyright: 93 | desc: Extract project and dependency licenses into asd copyright 94 | cmds: 95 | - | 96 | licensed notices 97 | 98 | cat > debian/arduino-router/usr/share/doc/arduino-router/copyright <> debian/arduino-router/usr/share/doc/arduino-router/copyright 116 | 117 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-dependencies-task/Taskfile.yml 118 | general:cache-dep-licenses: 119 | desc: Cache dependency license metadata 120 | deps: 121 | - task: general:prepare-deps 122 | cmds: 123 | - | 124 | if 125 | ! which licensed \ 126 | &>/dev/null 127 | then 128 | if [[ {{OS}} == "windows" ]]; then 129 | echo "Licensed does not have Windows support." 130 | echo "Please use Linux/macOS or download the dependencies cache from the GitHub Actions workflow artifact." 131 | else 132 | echo "licensed not found or not in PATH." 133 | echo "Please install: https://github.com/licensee/licensed#installation" 134 | fi 135 | exit 1 136 | fi 137 | - licensed cache 138 | 139 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-dependencies-task/Taskfile.yml 140 | general:check-dep-licenses: 141 | desc: Check for unapproved dependency licenses 142 | deps: 143 | - task: general:cache-dep-licenses 144 | cmds: 145 | - licensed status 146 | 147 | # Source: https://github.com/arduino/tooling-project-assets/blob/main/workflow-templates/assets/check-go-dependencies-task/Taskfile.yml 148 | general:prepare-deps: 149 | desc: Prepare project dependencies for license check 150 | # No preparation is needed for Go module-based projects. 151 | -------------------------------------------------------------------------------- /internal/monitorapi/monitor-api.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package monitorapi 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "log/slog" 22 | "net" 23 | "sync" 24 | "sync/atomic" 25 | "time" 26 | 27 | "github.com/djherbis/buffer" 28 | "github.com/djherbis/nio/v3" 29 | 30 | "github.com/arduino/arduino-router/internal/msgpackrouter" 31 | "github.com/arduino/arduino-router/msgpackrpc" 32 | ) 33 | 34 | var socketsLock sync.RWMutex 35 | var sockets map[net.Conn]struct{} 36 | var monSendPipeRd *nio.PipeReader 37 | var monSendPipeWr *nio.PipeWriter 38 | var bytesInSendPipe atomic.Int64 39 | 40 | // Register the Monitor API methods 41 | func Register(router *msgpackrouter.Router, addr string) error { 42 | listener, err := net.Listen("tcp", addr) 43 | if err != nil { 44 | return fmt.Errorf("failed to start listener: %w", err) 45 | } 46 | sockets = make(map[net.Conn]struct{}) 47 | monSendPipeRd, monSendPipeWr = nio.Pipe(buffer.New(1024)) 48 | 49 | go connectionHandler(listener) 50 | _ = router.RegisterMethod("mon/connected", connected) 51 | _ = router.RegisterMethod("mon/read", read) 52 | _ = router.RegisterMethod("mon/write", write) 53 | _ = router.RegisterMethod("mon/reset", reset) 54 | return nil 55 | } 56 | 57 | func connectionHandler(listener net.Listener) { 58 | for { 59 | conn, err := listener.Accept() 60 | if err != nil { 61 | slog.Error("Failed to accept monitor connection", "error", err) 62 | return 63 | } 64 | 65 | slog.Info("Accepted monitor connection", "from", conn.RemoteAddr()) 66 | socketsLock.Lock() 67 | sockets[conn] = struct{}{} 68 | socketsLock.Unlock() 69 | 70 | go func() { 71 | defer close(conn) 72 | 73 | // Read from the connection and write to the monitor send pipe 74 | buff := make([]byte, 1024) 75 | for { 76 | if n, err := conn.Read(buff); err != nil { 77 | // Connection closed from client 78 | return 79 | } else if written, err := monSendPipeWr.Write(buff[:n]); err != nil { 80 | return 81 | } else { 82 | bytesInSendPipe.Add(int64(written)) 83 | } 84 | } 85 | }() 86 | } 87 | } 88 | 89 | func connected(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 90 | if len(params) != 0 { 91 | return nil, []any{1, "Invalid number of parameters, expected no parameters"} 92 | } 93 | 94 | socketsLock.RLock() 95 | connected := len(sockets) > 0 96 | socketsLock.RUnlock() 97 | 98 | return connected, nil 99 | } 100 | 101 | func read(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 102 | if len(params) != 1 { 103 | return nil, []any{1, "Invalid number of parameters, expected max bytes to read"} 104 | } 105 | maxBytes, ok := msgpackrpc.ToUint(params[0]) 106 | if !ok { 107 | return nil, []any{1, "Invalid parameter type, expected positive int for max bytes to read"} 108 | } 109 | 110 | if bytesInSendPipe.Load() == 0 { 111 | return []byte{}, nil 112 | } 113 | 114 | buffer := make([]byte, maxBytes) 115 | if readed, err := monSendPipeRd.Read(buffer); err != nil { 116 | slog.Error("Error reading monitor", "error", err) 117 | return nil, []any{3, "Failed to read from connection: " + err.Error()} 118 | } else { 119 | bytesInSendPipe.Add(int64(-readed)) 120 | return buffer[:readed], nil 121 | } 122 | } 123 | 124 | func write(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 125 | if len(params) != 1 { 126 | return nil, []any{1, "Invalid number of parameters, expected data to write"} 127 | } 128 | data, ok := params[0].([]byte) 129 | if !ok { 130 | if dataStr, ok := params[0].(string); ok { 131 | data = []byte(dataStr) 132 | } else { 133 | // If data is not []byte or string, return an error 134 | return nil, []any{1, "Invalid parameter type, expected []byte or string for data to write"} 135 | } 136 | } 137 | 138 | socketsLock.RLock() 139 | clients := make([]net.Conn, 0, len(sockets)) 140 | for c := range sockets { 141 | clients = append(clients, c) 142 | } 143 | socketsLock.RUnlock() 144 | 145 | for _, conn := range clients { 146 | if len(clients) > 1 { 147 | // If there are multiple clients, allow 500 ms for the data to 148 | // get through each one. 149 | _ = conn.SetWriteDeadline(time.Now().Add(time.Millisecond * 500)) 150 | } else { 151 | _ = conn.SetWriteDeadline(time.Time{}) 152 | } 153 | if _, err := conn.Write(data); err != nil { 154 | // If we get an error, we assume the connection is lost. 155 | slog.Error("Monitor connection lost, closing connection", "error", err) 156 | close(conn) 157 | } 158 | } 159 | 160 | return len(data), nil 161 | } 162 | 163 | func close(conn net.Conn) { 164 | socketsLock.Lock() 165 | delete(sockets, conn) 166 | socketsLock.Unlock() 167 | _ = conn.Close() 168 | } 169 | 170 | func reset(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 171 | if len(params) != 0 { 172 | return nil, []any{1, "Invalid number of parameters, expected no parameters"} 173 | } 174 | 175 | socketsLock.Lock() 176 | socketsToClose := sockets 177 | sockets = make(map[net.Conn]struct{}) 178 | socketsLock.Unlock() 179 | 180 | for c := range socketsToClose { 181 | _ = c.Close() 182 | } 183 | 184 | slog.Info("Monitor connection reset") 185 | return true, nil 186 | } 187 | -------------------------------------------------------------------------------- /msgpackrpc/connection_rpc_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrpc 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "io" 22 | "sync" 23 | "testing" 24 | "time" 25 | 26 | "github.com/djherbis/buffer" 27 | "github.com/djherbis/nio/v3" 28 | "github.com/stretchr/testify/require" 29 | "github.com/vmihailenco/msgpack/v5" 30 | ) 31 | 32 | func TestRPCConnection(t *testing.T) { 33 | in, testdataIn := nio.Pipe(buffer.New(1024)) 34 | testdataOut, out := nio.Pipe(buffer.New(1024)) 35 | d := msgpack.NewDecoder(testdataOut) 36 | d.UseLooseInterfaceDecoding(true) 37 | type CustomError struct { 38 | Code int 39 | Message string 40 | } 41 | 42 | var wg sync.WaitGroup 43 | notification := "" 44 | request := "" 45 | requestError := "" 46 | conn := NewConnection( 47 | in, out, 48 | func(ctx context.Context, logger FunctionLogger, method string, params []any) (_result any, _err any) { 49 | defer wg.Done() 50 | request = fmt.Sprintf("REQ method=%v params=%v", method, params) 51 | if method == "tocancel" { 52 | select { 53 | case <-ctx.Done(): 54 | request += " canceled" 55 | case <-time.After(time.Second): 56 | request += " not canceled" 57 | t.Fail() 58 | } 59 | return nil, CustomError{Code: 1, Message: "error message"} 60 | } 61 | return []any{}, nil 62 | }, 63 | func(logger FunctionLogger, method string, params []any) { 64 | defer wg.Done() 65 | notification = fmt.Sprintf("NOT method=%v params=%v", method, params) 66 | }, 67 | func(e error) { 68 | defer wg.Done() 69 | if e == io.EOF { 70 | return 71 | } 72 | requestError = fmt.Sprintf("error=%s", e) 73 | }, 74 | ) 75 | t.Cleanup(func() { 76 | wg.Add(1) // this will produce an error in the callback handler 77 | conn.Close() 78 | }) 79 | go conn.Run() 80 | 81 | enc := msgpack.NewEncoder(testdataIn) 82 | enc.UseCompactInts(true) 83 | send := func(msg ...any) { 84 | require.NoError(t, enc.Encode(msg)) 85 | } 86 | sendCancel := func(id MessageID) { 87 | send(messageTypeNotification, "$/cancelRequest", []any{id}) 88 | } 89 | 90 | { // Test incoming notification 91 | wg.Add(1) 92 | send(messageTypeNotification, "initialized", []any{123}) 93 | wg.Wait() 94 | require.Equal(t, "NOT method=initialized params=[123]", notification) 95 | } 96 | 97 | { // Test incoming request 98 | wg.Add(1) 99 | send(messageTypeRequest, MessageID(1), "textDocument/didOpen", []any{}) 100 | wg.Wait() 101 | require.Equal(t, "REQ method=textDocument/didOpen params=[]", request) 102 | msg, err := d.DecodeSlice() 103 | require.NoError(t, err) 104 | require.Equal(t, []any{int64(1), int64(1), nil, []any{}}, msg) 105 | } 106 | 107 | { // Test another incoming request 108 | wg.Add(1) 109 | send(messageTypeRequest, MessageID(2), "textDocument/didClose", []any{}) 110 | wg.Wait() 111 | require.Equal(t, "REQ method=textDocument/didClose params=[]", request) 112 | msg, err := d.DecodeSlice() 113 | require.NoError(t, err) 114 | require.Equal(t, []any{int64(1), int64(2), nil, []any{}}, msg) 115 | } 116 | 117 | { // Test incoming request cancelation 118 | wg.Add(1) 119 | send(messageTypeRequest, MessageID(3), "tocancel", []any{}) 120 | time.Sleep(time.Millisecond * 100) 121 | sendCancel(3) 122 | wg.Wait() 123 | require.Equal(t, "REQ method=tocancel params=[] canceled", request) 124 | msg, err := d.DecodeSlice() 125 | require.NoError(t, err) 126 | require.Equal(t, []any{int64(1), int64(3), map[string]any{"Code": int64(1), "Message": "error message"}, nil}, msg) 127 | } 128 | 129 | { // Test outgoing request 130 | wg.Add(1) 131 | go func() { 132 | defer wg.Done() 133 | respRes, respErr, err := conn.SendRequest(t.Context(), "helloworld", true) 134 | require.NoError(t, err) 135 | require.Nil(t, respErr) 136 | require.Equal(t, map[string]any{"fakedata": int8(99)}, respRes) 137 | }() 138 | msg, err := d.DecodeSlice() // Grab the SendRequest 139 | require.NoError(t, err) 140 | require.Equal(t, []any{int64(0), int64(1), "helloworld", []any{true}}, msg) 141 | send(messageTypeResponse, 1, nil, map[string]any{"fakedata": 99}) 142 | wg.Wait() 143 | } 144 | 145 | { // Test invalid response 146 | wg.Add(1) 147 | send(1, 999, 10, nil) 148 | wg.Wait() 149 | require.Equal(t, "error=invalid ID in request response '999': double answer or request not sent", requestError) 150 | } 151 | } 152 | 153 | func TestRPCRougeDoubleCallWithSameID(t *testing.T) { 154 | in, testdataIn := nio.Pipe(buffer.New(1024)) 155 | testdataOut, out := nio.Pipe(buffer.New(1024)) 156 | 157 | enc := msgpack.NewEncoder(testdataIn) 158 | enc.UseCompactInts(true) 159 | send := func(msg ...any) { 160 | require.NoError(t, enc.Encode(msg)) 161 | } 162 | 163 | d := msgpack.NewDecoder(testdataOut) 164 | d.UseLooseInterfaceDecoding(true) 165 | var reqLock sync.Mutex 166 | request := "" 167 | requestError := "" 168 | var wg sync.WaitGroup 169 | conn := NewConnection( 170 | in, out, 171 | func(ctx context.Context, logger FunctionLogger, method string, params []any) (_result any, _err any) { 172 | defer wg.Done() 173 | reqLock.Lock() 174 | request += fmt.Sprintf("REQ method=%v params=%v\n", method, params) 175 | reqLock.Unlock() 176 | time.Sleep(500 * time.Millisecond) // Simulate a long request 177 | return params[0], nil 178 | }, 179 | nil, 180 | func(e error) { 181 | if e == io.EOF { 182 | return 183 | } 184 | reqLock.Lock() 185 | requestError = fmt.Sprintf("error=%s", e) 186 | reqLock.Unlock() 187 | }, 188 | ) 189 | go conn.Run() 190 | t.Cleanup(conn.Close) 191 | 192 | wg.Add(2) 193 | send(messageTypeRequest, MessageID(1), "test", []any{1}) 194 | time.Sleep(100 * time.Millisecond) 195 | send(messageTypeRequest, MessageID(1), "test", []any{2}) 196 | wg.Wait() 197 | require.Equal(t, "REQ method=test params=[1]\nREQ method=test params=[2]\n", request) 198 | require.Equal(t, "error=RPC protocol violation: request with ID 1 already active, canceling it", requestError) 199 | 200 | time.Sleep(100 * time.Millisecond) 201 | res, err := d.DecodeInterface() 202 | require.NoError(t, err) 203 | // Expect answer from the second request only 204 | require.Equal(t, []any{int64(1), int64(1), nil, int64(2)}, res) 205 | } 206 | -------------------------------------------------------------------------------- /internal/hciapi/hci-api.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package hciapi 17 | 18 | import ( 19 | "context" 20 | "encoding/hex" 21 | "errors" 22 | "fmt" 23 | "log/slog" 24 | "strconv" 25 | "sync/atomic" 26 | 27 | "golang.org/x/sys/unix" 28 | 29 | "github.com/arduino/arduino-router/internal/msgpackrouter" 30 | "github.com/arduino/arduino-router/msgpackrpc" 31 | ) 32 | 33 | var hciSocket atomic.Int32 34 | 35 | //nolint:gochecknoinits 36 | func init() { 37 | hciSocket.Store(-1) 38 | } 39 | 40 | // Register registers the HCI API methods with the router. 41 | func Register(router *msgpackrouter.Router) { 42 | _ = router.RegisterMethod("hci/open", HCIOpen) 43 | _ = router.RegisterMethod("hci/send", HCISend) 44 | _ = router.RegisterMethod("hci/recv", HCIRecv) 45 | _ = router.RegisterMethod("hci/avail", HCIAvail) 46 | _ = router.RegisterMethod("hci/close", HCIClose) 47 | } 48 | 49 | // HCIOpen opens an HCI socket bound to the specified device (e.g. "hci0"). 50 | func HCIOpen(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) { 51 | if len(params) != 1 { 52 | return nil, []any{1, "Expected one parameter: HCI device name (e.g., 'hci0')"} 53 | } 54 | 55 | deviceName, ok := params[0].(string) 56 | if !ok { 57 | return nil, []any{1, "Invalid parameter type: expected string for device name"} 58 | } 59 | 60 | if len(deviceName) < 4 || deviceName[:3] != "hci" { 61 | return nil, []any{1, "Invalid device name format, expected 'hciX' where X is device number"} 62 | } 63 | 64 | devNum, err := strconv.Atoi(deviceName[3:]) 65 | if err != nil || devNum < 0 || devNum > 0xFFFF { 66 | return nil, []any{1, "Invalid device number in device name"} 67 | } 68 | 69 | // Close any existing socket 70 | if fd := hciSocket.Swap(-1); fd >= 0 { 71 | _ = unix.Close(int(fd)) 72 | } 73 | 74 | // Create raw HCI socket 75 | fd, err := unix.Socket(unix.AF_BLUETOOTH, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.BTPROTO_HCI) 76 | if err != nil { 77 | return nil, []any{3, fmt.Sprintf("Failed to create HCI socket: %v", err)} 78 | } 79 | 80 | // Bring down the HCI device using ioctl (HCIDEVDOWN) 81 | const HCIDEVDOWN = 0x400448CA // from 82 | 83 | if err := unix.IoctlSetInt(fd, HCIDEVDOWN, devNum); err != nil { 84 | unix.Close(fd) 85 | return nil, []any{3, "Failed to bring down HCI device: " + err.Error()} 86 | } 87 | slog.Info("Brought down HCI device", "device", deviceName) 88 | 89 | // Bind to device (user channel) 90 | addr := &unix.SockaddrHCI{ 91 | Dev: uint16(devNum), //nolint:gosec 92 | Channel: unix.HCI_CHANNEL_USER, 93 | } 94 | 95 | if err := unix.Bind(fd, addr); err != nil { 96 | unix.Close(fd) 97 | return nil, []any{3, fmt.Sprintf("Failed to bind to HCI device: %v", err)} 98 | } 99 | 100 | hciSocket.Store(int32(fd)) //nolint:gosec 101 | slog.Info("Opened HCI device", "device", deviceName, "fd", fd) 102 | return true, nil 103 | } 104 | 105 | // HCIClose closes the currently open HCI socket. 106 | func HCIClose(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) { 107 | if len(params) != 0 { 108 | return nil, []any{1, "Expected no parameters"} 109 | } 110 | 111 | if fd := hciSocket.Swap(-1); fd >= 0 { 112 | unix.Close(int(fd)) 113 | } 114 | 115 | slog.Info("Closed HCI device") 116 | return true, nil 117 | } 118 | 119 | // HCISend transmits raw data to the open HCI socket. 120 | func HCISend(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) { 121 | if len(params) != 1 { 122 | return nil, []any{1, "Expected one parameter: data to send"} 123 | } 124 | 125 | var data []byte 126 | switch v := params[0].(type) { 127 | case []byte: 128 | data = v 129 | case string: 130 | data = []byte(v) 131 | default: 132 | return nil, []any{1, "Invalid parameter type, expected []byte or string"} 133 | } 134 | 135 | fd := hciSocket.Load() 136 | if fd < 0 { 137 | return nil, []any{2, "No HCI device open"} 138 | } 139 | 140 | n, err := unix.Write(int(fd), data) 141 | if err != nil { 142 | slog.Error("Failed to send HCI packet", "err", err) 143 | return nil, []any{3, fmt.Sprintf("Failed to send HCI packet: %v", err)} 144 | } 145 | 146 | if slog.Default().Enabled(context.Background(), slog.LevelDebug) { 147 | slog.Debug("Sent HCI packet", "bytes", n, "data", hex.EncodeToString(data)) 148 | } 149 | return n, nil 150 | } 151 | 152 | // HCIRecv reads available data from the HCI socket. 153 | func HCIRecv(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) { 154 | if len(params) != 1 { 155 | return nil, []any{1, "Expected one parameter: max bytes to receive"} 156 | } 157 | 158 | size, ok := msgpackrpc.ToUint(params[0]) 159 | if !ok { 160 | return nil, []any{1, "Invalid parameter type, expected uint for max bytes"} 161 | } 162 | 163 | fd := hciSocket.Load() 164 | if fd < 0 { 165 | return nil, []any{2, "No HCI device open"} 166 | } 167 | 168 | buffer := make([]byte, size) 169 | 170 | // Short timeout (1ms) for non-blocking behavior 171 | tv := unix.Timeval{Usec: 1000} 172 | if err := unix.SetsockoptTimeval(int(fd), unix.SOL_SOCKET, unix.SO_RCVTIMEO, &tv); err != nil { 173 | return nil, []any{3, fmt.Sprintf("Failed to set read timeout: %v", err)} 174 | } 175 | 176 | n, err := unix.Read(int(fd), buffer) 177 | if err != nil { 178 | if errors.Is(err, unix.EAGAIN) || errors.Is(err, unix.EWOULDBLOCK) { 179 | slog.Debug("HCI recv timeout - no data available") 180 | return []byte{}, nil 181 | } 182 | slog.Error("Failed to receive HCI packet", "err", err) 183 | return nil, []any{3, fmt.Sprintf("Failed to receive HCI packet: %v", err)} 184 | } 185 | 186 | if slog.Default().Enabled(context.Background(), slog.LevelDebug) { 187 | slog.Debug("Received HCI packet", "bytes", n, "data", hex.EncodeToString(buffer[:n])) 188 | } 189 | return buffer[:n], nil 190 | } 191 | 192 | // HCIAvail checks whether data is available to read on the HCI socket. 193 | func HCIAvail(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_ any, _ any) { 194 | if len(params) != 0 { 195 | return nil, []any{1, "Expected no parameters"} 196 | } 197 | 198 | fd := hciSocket.Load() 199 | if fd < 0 { 200 | return nil, []any{2, "No HCI device open"} 201 | } 202 | 203 | fds := []unix.PollFd{{ 204 | Fd: fd, 205 | Events: unix.POLLIN, 206 | }} 207 | 208 | n, err := unix.Poll(fds, 0) 209 | if err != nil { 210 | if errors.Is(err, unix.EINTR) { 211 | return false, nil 212 | } 213 | slog.Error("Failed to poll HCI socket", "err", err) 214 | return nil, []any{3, fmt.Sprintf("Poll failed: %v", err)} 215 | } 216 | 217 | return n > 0 && (fds[0].Revents&unix.POLLIN) != 0, nil 218 | } 219 | -------------------------------------------------------------------------------- /internal/msgpackrouter/router_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrouter_test 17 | 18 | import ( 19 | "bytes" 20 | "context" 21 | "fmt" 22 | "io" 23 | "sync" 24 | "testing" 25 | "time" 26 | 27 | "github.com/arduino/arduino-router/internal/msgpackrouter" 28 | "github.com/arduino/arduino-router/msgpackrpc" 29 | 30 | "github.com/djherbis/buffer" 31 | "github.com/djherbis/nio/v3" 32 | "github.com/stretchr/testify/require" 33 | ) 34 | 35 | type FullPipe struct { 36 | in *nio.PipeReader 37 | out *nio.PipeWriter 38 | } 39 | 40 | func (p *FullPipe) Read(b []byte) (int, error) { 41 | return p.in.Read(b) 42 | } 43 | 44 | func (p *FullPipe) Write(b []byte) (int, error) { 45 | return p.out.Write(b) 46 | } 47 | 48 | func (p *FullPipe) Close() error { 49 | err1 := p.out.Close() 50 | err2 := p.in.Close() 51 | if err1 != nil { 52 | return err1 53 | } 54 | if err2 != nil { 55 | return err2 56 | } 57 | return nil 58 | } 59 | 60 | func newFullPipe() (io.ReadWriteCloser, io.ReadWriteCloser) { 61 | in1, out1 := nio.Pipe(buffer.New(1024)) 62 | in2, out2 := nio.Pipe(buffer.New(1024)) 63 | return &FullPipe{in1, out2}, &FullPipe{in2, out1} 64 | } 65 | 66 | func TestBasicRouterFunctionality(t *testing.T) { 67 | ch1a, ch1b := newFullPipe() 68 | ch2a, ch2b := newFullPipe() 69 | 70 | cl1NotificationsMux := sync.Mutex{} 71 | cl1Notifications := bytes.NewBuffer(nil) 72 | 73 | cl1 := msgpackrpc.NewConnection(ch1a, ch1a, func(ctx context.Context, logger msgpackrpc.FunctionLogger, method string, params []any) (_result any, _err any) { 74 | switch method { 75 | case "ping": 76 | return params, nil 77 | default: 78 | return nil, "unknown method: " + method 79 | } 80 | }, func(logger msgpackrpc.FunctionLogger, method string, params []any) { 81 | cl1NotificationsMux.Lock() 82 | fmt.Fprintf(cl1Notifications, "notification: %s %+v\n", method, params) 83 | cl1NotificationsMux.Unlock() 84 | }, func(err error) { 85 | }) 86 | go cl1.Run() 87 | 88 | cl2 := msgpackrpc.NewConnection(ch2a, ch2a, func(ctx context.Context, logger msgpackrpc.FunctionLogger, method string, params []any) (result any, err any) { 89 | return nil, nil 90 | }, func(logger msgpackrpc.FunctionLogger, method string, params []any) { 91 | }, func(err error) { 92 | }) 93 | go cl2.Run() 94 | 95 | router := msgpackrouter.New(0) 96 | router.Accept(ch1b) 97 | router.Accept(ch2b) 98 | 99 | { 100 | // Register a method on the first client 101 | result, reqErr, err := cl1.SendRequest(context.Background(), "$/register", "ping") 102 | require.Equal(t, true, result) 103 | require.Nil(t, reqErr) 104 | require.NoError(t, err) 105 | } 106 | { 107 | // Try to re-register the same method 108 | result, reqErr, err := cl1.SendRequest(context.Background(), "$/register", "ping") 109 | require.Nil(t, result) 110 | require.Equal(t, []any{int8(msgpackrouter.ErrCodeRouteAlreadyExists), "route already exists: ping"}, reqErr) 111 | require.NoError(t, err) 112 | } 113 | { 114 | // Register a method on the second client 115 | result, reqErr, err := cl2.SendRequest(context.Background(), "$/register", "temperature") 116 | require.Equal(t, true, result) 117 | require.Nil(t, reqErr) 118 | require.NoError(t, err) 119 | } 120 | { 121 | // Call from client2 the registered method on client1 122 | result, reqErr, err := cl2.SendRequest(context.Background(), "ping", "1", 2, true) 123 | require.Equal(t, []any{"1", int8(2), true}, result) 124 | require.Nil(t, reqErr) 125 | require.NoError(t, err) 126 | } 127 | { 128 | // Self-call from client1 129 | result, reqErr, err := cl1.SendRequest(context.Background(), "ping", "c", 12, false) 130 | require.Equal(t, []any{"c", int8(12), false}, result) 131 | require.Nil(t, reqErr) 132 | require.NoError(t, err) 133 | } 134 | { 135 | // Call from client2 an un-registered method 136 | result, reqErr, err := cl2.SendRequest(context.Background(), "not-existent-method", "1", 2, true) 137 | require.Nil(t, result) 138 | require.Equal(t, []any{int8(msgpackrouter.ErrCodeMethodNotAvailable), "method not-existent-method not available"}, reqErr) 139 | require.NoError(t, err) 140 | } 141 | { 142 | // Send notification to client1 143 | err := cl2.SendNotification("ping", "a", int16(4), false) 144 | require.NoError(t, err) 145 | } 146 | { 147 | // Send notification to unregistered method 148 | err := cl2.SendNotification("notexistent", "a", int16(4), false) 149 | require.NoError(t, err) 150 | } 151 | { 152 | // Self-send notification 153 | err := cl1.SendNotification("ping", "b", int16(14), true, true) 154 | require.NoError(t, err) 155 | } 156 | time.Sleep(100 * time.Millisecond) // Give some time for the notifications to be processed 157 | 158 | cl1NotificationsMux.Lock() 159 | require.Contains(t, cl1Notifications.String(), "notification: ping [a 4 false]") 160 | require.Contains(t, cl1Notifications.String(), "notification: ping [b 14 true true]") 161 | cl1NotificationsMux.Unlock() 162 | } 163 | 164 | func TestMessageForwarderCongestionControl(t *testing.T) { 165 | // Test parameters 166 | queueSize := 5 167 | msgLatency := 100 * time.Millisecond 168 | // Run a batch of 20 requests, and expect them to take more than 400 ms 169 | // in total because the router should throttle requests in batch of 5. 170 | batchSize := queueSize * 4 171 | expectedLatency := msgLatency * time.Duration(batchSize/queueSize) 172 | 173 | // Make a client that simulates a slow response 174 | ch1a, ch1b := newFullPipe() 175 | cl1 := msgpackrpc.NewConnection(ch1a, ch1a, func(ctx context.Context, logger msgpackrpc.FunctionLogger, method string, params []any) (_result any, _err any) { 176 | time.Sleep(msgLatency) 177 | return true, nil 178 | }, nil, nil) 179 | go cl1.Run() 180 | 181 | // Make a second client to send requests, without any delay 182 | ch2a, ch2b := newFullPipe() 183 | cl2 := msgpackrpc.NewConnection(ch2a, ch2a, nil, nil, nil) 184 | go cl2.Run() 185 | 186 | // Setup router 187 | router := msgpackrouter.New(queueSize) // max 5 pending messages per connection 188 | router.Accept(ch1b) 189 | router.Accept(ch2b) 190 | 191 | { 192 | // Register a method on the first client 193 | result, reqErr, err := cl1.SendRequest(context.Background(), "$/register", "test") 194 | require.Equal(t, true, result) 195 | require.Nil(t, reqErr) 196 | require.NoError(t, err) 197 | } 198 | 199 | // Run batch of requests from cl2 to cl1 200 | start := time.Now() 201 | var wg sync.WaitGroup 202 | for range batchSize { 203 | wg.Go(func() { 204 | _, _, err := cl2.SendRequest(t.Context(), "test") 205 | require.NoError(t, err) 206 | }) 207 | } 208 | wg.Wait() 209 | elapsed := time.Since(start) 210 | 211 | // Check that the elapsed time is greater than expectedLatency 212 | fmt.Println("Elapsed time for requests:", elapsed) 213 | require.Greater(t, elapsed, expectedLatency, "Expected elapsed time to be greater than %s", expectedLatency) 214 | } 215 | -------------------------------------------------------------------------------- /internal/msgpackrouter/router.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrouter 17 | 18 | import ( 19 | "context" 20 | "errors" 21 | "fmt" 22 | "io" 23 | "log/slog" 24 | "maps" 25 | "sync" 26 | 27 | "github.com/arduino/arduino-router/msgpackrpc" 28 | ) 29 | 30 | type RouterRequestHandler func(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (result any, err any) 31 | 32 | type Router struct { 33 | routesLock sync.Mutex 34 | routes map[string]*msgpackrpc.Connection 35 | routesInternal map[string]RouterRequestHandler 36 | sendMaxWorkers int 37 | } 38 | 39 | func New(perConnMaxWorkers int) *Router { 40 | return &Router{ 41 | routes: make(map[string]*msgpackrpc.Connection), 42 | routesInternal: make(map[string]RouterRequestHandler), 43 | sendMaxWorkers: perConnMaxWorkers, 44 | } 45 | } 46 | 47 | // SetSendMaxWorkers sets the maximum number of workers for sending on each connection, 48 | // this value limits the number of concurrent requests that can be sent on each connection. 49 | // A value of 0 means unlimited workers. 50 | // Only new connections will be affected by this change, existing connections 51 | // will keep their current sendMaxWorkers value. 52 | func (r *Router) SetSendMaxWorkers(size int) { 53 | r.routesLock.Lock() 54 | defer r.routesLock.Unlock() 55 | r.sendMaxWorkers = size 56 | } 57 | 58 | func (r *Router) Accept(conn io.ReadWriteCloser) <-chan struct{} { 59 | res := make(chan struct{}) 60 | go func() { 61 | r.connectionLoop(conn) 62 | close(res) 63 | }() 64 | return res 65 | } 66 | 67 | func (r *Router) RegisterMethod(method string, handler RouterRequestHandler) error { 68 | r.routesLock.Lock() 69 | defer r.routesLock.Unlock() 70 | 71 | if _, ok := r.routesInternal[method]; ok { 72 | slog.Error("Route already exists", "method", method) 73 | return newRouteAlreadyExistsError(method) 74 | } 75 | 76 | // Register the method with the handler 77 | r.routesInternal[method] = handler 78 | slog.Info("Registered internal method", "method", method) 79 | return nil 80 | } 81 | 82 | func (r *Router) connectionLoop(conn io.ReadWriteCloser) { 83 | defer conn.Close() 84 | 85 | var msgpackconn *msgpackrpc.Connection 86 | msgpackconn = msgpackrpc.NewConnectionWithMaxWorkers(conn, conn, 87 | func(ctx context.Context, _ msgpackrpc.FunctionLogger, method string, params []any) (_result any, _err any) { 88 | // This handler is called when a request is received from the client 89 | slog.Debug("Received request", "method", method, "params", params) 90 | defer func() { 91 | slog.Debug("Received response", "method", method, "result", _result, "error", _err) 92 | }() 93 | 94 | switch method { 95 | case "$/register": 96 | // Check if the client is trying to register a new method 97 | if len(params) != 1 { 98 | return nil, routerError(ErrCodeInvalidParams, fmt.Sprintf("invalid params: only one param is expected, got %d", len(params))) 99 | } else if methodToRegister, ok := params[0].(string); !ok { 100 | return nil, routerError(ErrCodeInvalidParams, fmt.Sprintf("invalid params: expected string, got %T", params[0])) 101 | } else if err := r.registerMethod(methodToRegister, msgpackconn); err != nil { 102 | if rae, ok := err.(*RouteError); ok { 103 | return nil, rae.ToEncodedError() 104 | } 105 | return nil, routerError(ErrCodeGenericError, err.Error()) 106 | } else { 107 | return true, nil 108 | } 109 | case "$/reset": 110 | // Check if the client is trying to remove its registered methods 111 | if len(params) != 0 { 112 | return nil, routerError(ErrCodeInvalidParams, "invalid params: no params are expected") 113 | } else { 114 | r.removeMethodsFromConnection(msgpackconn) 115 | return true, nil 116 | } 117 | } 118 | 119 | // Check if the method is an internal method 120 | if handler, ok := r.routesInternal[method]; ok { 121 | // Call the internal method handler 122 | return handler(ctx, msgpackconn, params) 123 | } 124 | 125 | // Check if the method is registered 126 | client, ok := r.getConnectionForMethod(method) 127 | if !ok { 128 | return nil, routerError(ErrCodeMethodNotAvailable, fmt.Sprintf("method %s not available", method)) 129 | } 130 | 131 | // Forward the call to the registered client 132 | reqResult, reqError, err := client.SendRequest(ctx, method, params...) 133 | if err != nil { 134 | slog.Error("Failed to send request", "method", method, "err", err) 135 | return nil, routerError(ErrCodeFailedToSendRequests, fmt.Sprintf("failed to send request: %s", err)) 136 | } 137 | 138 | // Send the response back to the original caller 139 | return reqResult, reqError 140 | }, 141 | func(_ msgpackrpc.FunctionLogger, method string, params []any) { 142 | // This handler is called when a notification is received from the client 143 | slog.Debug("Received notification", "method", method, "params", params) 144 | 145 | // Check if the method is an internal method 146 | if handler, ok := r.routesInternal[method]; ok { 147 | // call the internal method handler (since it's a notification, discard the result) 148 | _, _ = handler(context.Background(), msgpackconn, params) 149 | return 150 | } 151 | 152 | // Check if the method is registered 153 | client, ok := r.getConnectionForMethod(method) 154 | if !ok { 155 | // if the method is not registered, the notifitication is lost 156 | return 157 | } 158 | 159 | // Forward the notification to the registered client 160 | if err := client.SendNotification(method, params...); err != nil { 161 | slog.Error("Failed to send notification", "method", method, "err", err) 162 | return 163 | } 164 | }, 165 | func(err error) { 166 | if errors.Is(err, io.EOF) { 167 | slog.Info("Connection closed by peer") 168 | return 169 | } 170 | slog.Error("Error in connection", "err", err) 171 | }, 172 | r.sendMaxWorkers, 173 | ) 174 | 175 | msgpackconn.Run() 176 | 177 | // Unregister the methods when the connection is terminated 178 | r.removeMethodsFromConnection(msgpackconn) 179 | msgpackconn.Close() 180 | 181 | } 182 | 183 | func (r *Router) registerMethod(method string, conn *msgpackrpc.Connection) error { 184 | r.routesLock.Lock() 185 | defer r.routesLock.Unlock() 186 | 187 | if _, ok := r.routes[method]; ok { 188 | return newRouteAlreadyExistsError(method) 189 | } 190 | r.routes[method] = conn 191 | return nil 192 | } 193 | 194 | func (r *Router) removeMethodsFromConnection(conn *msgpackrpc.Connection) { 195 | r.routesLock.Lock() 196 | defer r.routesLock.Unlock() 197 | 198 | maps.DeleteFunc(r.routes, func(k string, v *msgpackrpc.Connection) bool { 199 | return v == conn 200 | }) 201 | } 202 | 203 | func (r *Router) getConnectionForMethod(method string) (*msgpackrpc.Connection, bool) { 204 | r.routesLock.Lock() 205 | defer r.routesLock.Unlock() 206 | conn, ok := r.routes[method] 207 | return conn, ok 208 | } 209 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package main 17 | 18 | import ( 19 | "cmp" 20 | "context" 21 | "encoding/hex" 22 | "fmt" 23 | "io" 24 | "log/slog" 25 | "net" 26 | "os" 27 | "os/signal" 28 | "sync" 29 | "syscall" 30 | "time" 31 | 32 | "github.com/arduino/arduino-router/internal/hciapi" 33 | "github.com/arduino/arduino-router/internal/monitorapi" 34 | "github.com/arduino/arduino-router/internal/msgpackrouter" 35 | networkapi "github.com/arduino/arduino-router/internal/network-api" 36 | "github.com/arduino/arduino-router/msgpackrpc" 37 | 38 | "github.com/spf13/cobra" 39 | "go.bug.st/f" 40 | "go.bug.st/serial" 41 | ) 42 | 43 | // Version will be set a build time with -ldflags 44 | var Version string = "0.0.0-dev" 45 | 46 | // Server configuration 47 | type Config struct { 48 | LogLevel slog.Level 49 | ListenTCPAddr string 50 | ListenUnixAddr string 51 | SerialPortAddr string 52 | SerialBaudRate int 53 | MonitorPortAddr string 54 | MaxPendingRequestsPerClient int 55 | } 56 | 57 | func main() { 58 | var cfg Config 59 | var verbose bool 60 | cmd := &cobra.Command{ 61 | Use: "arduino-router", 62 | Long: "Arduino router for msgpack RPC service protocol", 63 | Run: func(cmd *cobra.Command, args []string) { 64 | if verbose { 65 | cfg.LogLevel = slog.LevelDebug 66 | } else { 67 | cfg.LogLevel = slog.LevelInfo 68 | } 69 | if !cmd.Flags().Changed("unix-port") { 70 | cfg.ListenUnixAddr = cmp.Or(os.Getenv("ARDUINO_ROUTER_SOCKET"), cfg.ListenUnixAddr) 71 | } 72 | if err := startRouter(cfg); err != nil { 73 | slog.Error("Failed to start router", "err", err) 74 | os.Exit(1) 75 | } 76 | }, 77 | } 78 | cmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose logging") 79 | cmd.Flags().StringVarP(&cfg.ListenTCPAddr, "listen-port", "l", "", "Listening port for RPC services") 80 | cmd.Flags().StringVarP(&cfg.ListenUnixAddr, "unix-port", "u", "/var/run/arduino-router.sock", "Listening port for RPC services") 81 | cmd.Flags().StringVarP(&cfg.SerialPortAddr, "serial-port", "p", "", "Serial port address") 82 | cmd.Flags().IntVarP(&cfg.SerialBaudRate, "serial-baudrate", "b", 115200, "Serial port baud rate") 83 | cmd.Flags().StringVarP(&cfg.MonitorPortAddr, "monitor-port", "m", "127.0.0.1:7500", "Listening port for MCU monitor proxy") 84 | cmd.Flags().IntVarP(&cfg.MaxPendingRequestsPerClient, "max-pending-requests", "", 25, "Maximum number of pending requests per client connection (0 = unlimited)") 85 | cmd.AddCommand(&cobra.Command{ 86 | Use: "version", 87 | Long: "Print version information", 88 | Run: func(cmd *cobra.Command, args []string) { 89 | fmt.Println("Arduino Router " + Version) 90 | }, 91 | }) 92 | 93 | if err := cmd.Execute(); err != nil { 94 | slog.Error("Error executing command.", "error", err) 95 | } 96 | } 97 | 98 | type MsgpackDebugStream struct { 99 | Upstream io.ReadWriteCloser 100 | Name string 101 | } 102 | 103 | func (d *MsgpackDebugStream) Read(p []byte) (n int, err error) { 104 | n, err = d.Upstream.Read(p) 105 | if err != nil { 106 | slog.Debug("Read error from "+d.Name, "err", err) 107 | } else { 108 | slog.Debug("Read from "+d.Name, "data", hex.EncodeToString(p[:n])) 109 | } 110 | return n, err 111 | } 112 | 113 | func (d *MsgpackDebugStream) Write(p []byte) (n int, err error) { 114 | n, err = d.Upstream.Write(p) 115 | if err != nil { 116 | slog.Debug("Write error to "+d.Name, "err", err) 117 | } else { 118 | slog.Debug("Write to "+d.Name, "data", hex.EncodeToString(p[:n])) 119 | } 120 | return n, err 121 | } 122 | 123 | func (d *MsgpackDebugStream) Close() error { 124 | return d.Upstream.Close() 125 | } 126 | 127 | func startRouter(cfg Config) error { 128 | slog.SetLogLoggerLevel(cfg.LogLevel) 129 | 130 | var listeners []net.Listener 131 | 132 | // Open listening TCP socket 133 | if cfg.ListenTCPAddr != "" { 134 | if l, err := net.Listen("tcp", cfg.ListenTCPAddr); err != nil { 135 | return fmt.Errorf("failed to listen on TCP port %s: %w", cfg.ListenTCPAddr, err) 136 | } else { 137 | slog.Info("Listening on TCP socket", "listen_addr", cfg.ListenTCPAddr) 138 | listeners = append(listeners, l) 139 | } 140 | } 141 | 142 | // Open listening UNIX socket 143 | if cfg.ListenUnixAddr != "" { 144 | _ = os.Remove(cfg.ListenUnixAddr) // Remove the socket file if it exists 145 | if l, err := net.Listen("unix", cfg.ListenUnixAddr); err != nil { 146 | return fmt.Errorf("failed to listen on UNIX socket %s: %w", cfg.ListenUnixAddr, err) 147 | } else { 148 | slog.Info("Listening on Unix socket", "listen_addr", cfg.ListenUnixAddr) 149 | listeners = append(listeners, l) 150 | } 151 | 152 | // Allow `arduino` user to write to a socket file owned by `root` 153 | if err := os.Chmod(cfg.ListenUnixAddr, 0666); err != nil { 154 | return err 155 | } 156 | } 157 | 158 | // Run router 159 | router := msgpackrouter.New(cfg.MaxPendingRequestsPerClient) 160 | 161 | // Register TCP network API methods 162 | networkapi.Register(router) 163 | 164 | // Register HCI API methods 165 | hciapi.Register(router) 166 | 167 | // Register monitor version API methods 168 | if err := router.RegisterMethod("$/version", func(_ context.Context, _ *msgpackrpc.Connection, _ []any) (any, any) { 169 | return Version, nil 170 | }); err != nil { 171 | slog.Error("Failed to register version API", "err", err) 172 | } 173 | 174 | // Register monitor API methods 175 | if err := monitorapi.Register(router, cfg.MonitorPortAddr); err != nil { 176 | slog.Error("Failed to register monitor API", "err", err) 177 | } 178 | 179 | // Open serial port if specified 180 | if cfg.SerialPortAddr != "" { 181 | var serialLock sync.Mutex 182 | var serialOpened = sync.NewCond(&serialLock) 183 | var serialClosed = sync.NewCond(&serialLock) 184 | var serialCloseSignal = make(chan struct{}) 185 | err := router.RegisterMethod("$/serial/open", func(ctx context.Context, _ *msgpackrpc.Connection, params []any) (result any, err any) { 186 | if len(params) != 1 { 187 | return nil, []any{1, "Invalid number of parameters"} 188 | } 189 | address, ok := params[0].(string) 190 | if !ok { 191 | return nil, []any{1, "Invalid parameter type"} 192 | } 193 | slog.Info("Request for opening serial port", "serial", address) 194 | if address != cfg.SerialPortAddr { 195 | return nil, []any{1, "Invalid serial port address"} 196 | } 197 | serialOpened.L.Lock() 198 | if serialCloseSignal == nil { // check if already opened 199 | serialCloseSignal = make(chan struct{}) 200 | serialOpened.Broadcast() 201 | } 202 | serialOpened.L.Unlock() 203 | return true, nil 204 | }) 205 | f.Assert(err == nil, "Failed to register $/serial/open method") 206 | err = router.RegisterMethod("$/serial/close", func(ctx context.Context, _ *msgpackrpc.Connection, params []any) (result any, err any) { 207 | if len(params) != 1 { 208 | return nil, []any{1, "Invalid number of parameters"} 209 | } 210 | address, ok := params[0].(string) 211 | if !ok { 212 | return nil, []any{1, "Invalid parameter type"} 213 | } 214 | slog.Info("Request for closing serial port", "serial", address) 215 | if address != cfg.SerialPortAddr { 216 | return nil, []any{1, "Invalid serial port address"} 217 | } 218 | serialClosed.L.Lock() 219 | if serialCloseSignal != nil { // check if already closed 220 | close(serialCloseSignal) 221 | serialCloseSignal = nil 222 | serialClosed.Wait() 223 | } 224 | serialClosed.L.Unlock() 225 | return true, nil 226 | }) 227 | f.Assert(err == nil, "Failed to register $/serial/close method") 228 | go func() { 229 | for { 230 | serialOpened.L.Lock() 231 | for serialCloseSignal == nil { 232 | serialClosed.Broadcast() 233 | serialOpened.Wait() 234 | } 235 | close := serialCloseSignal 236 | serialOpened.L.Unlock() 237 | 238 | slog.Info("Opening serial connection", "serial", cfg.SerialPortAddr) 239 | serialPort, err := serial.Open(cfg.SerialPortAddr, &serial.Mode{ 240 | BaudRate: cfg.SerialBaudRate, 241 | DataBits: 8, 242 | StopBits: serial.OneStopBit, 243 | Parity: serial.NoParity, 244 | }) 245 | if err != nil { 246 | slog.Error("Failed to open serial port. Retrying in 5 seconds...", "serial", cfg.SerialPortAddr, "err", err) 247 | time.Sleep(5 * time.Second) 248 | continue 249 | } 250 | slog.Info("Opened serial connection", "serial", cfg.SerialPortAddr) 251 | wr := &MsgpackDebugStream{Name: cfg.SerialPortAddr, Upstream: serialPort} 252 | 253 | // wait for the close command from RPC or for a failure of the serial port (routerExit) 254 | routerExit := router.Accept(wr) 255 | select { 256 | case <-routerExit: 257 | slog.Info("Serial port failed connection") 258 | case <-close: 259 | } 260 | 261 | // in any case, wait for the router to drop the connection 262 | serialPort.Close() 263 | <-routerExit 264 | } 265 | }() 266 | } 267 | 268 | // Wait for incoming connections on all listeners 269 | for _, l := range listeners { 270 | go func() { 271 | for { 272 | conn, err := l.Accept() 273 | if err != nil { 274 | slog.Error("Failed to accept connection", "err", err) 275 | break 276 | } 277 | 278 | slog.Info("Accepted connection", "addr", conn.RemoteAddr()) 279 | router.Accept(conn) 280 | } 281 | }() 282 | } 283 | 284 | // Sleep forever until interrupted 285 | signalChan := make(chan os.Signal, 1) 286 | signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) 287 | <-signalChan 288 | 289 | // Perform graceful shutdown 290 | for _, l := range listeners { 291 | slog.Info("Closing listener", "addr", l.Addr()) 292 | if err := l.Close(); err != nil { 293 | slog.Error("Failed to close listener", "err", err) 294 | } 295 | } 296 | 297 | return nil 298 | } 299 | -------------------------------------------------------------------------------- /.licenses/arduino-router/go/github.com/spf13/cobra.dep.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: github.com/spf13/cobra 3 | version: v1.10.1 4 | type: go 5 | summary: Package cobra is a commander providing a simple interface to create powerful 6 | modern CLI interfaces. 7 | homepage: https://pkg.go.dev/github.com/spf13/cobra 8 | license: apache-2.0 9 | licenses: 10 | - sources: LICENSE.txt 11 | text: |2 12 | Apache License 13 | Version 2.0, January 2004 14 | http://www.apache.org/licenses/ 15 | 16 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 17 | 18 | 1. Definitions. 19 | 20 | "License" shall mean the terms and conditions for use, reproduction, 21 | and distribution as defined by Sections 1 through 9 of this document. 22 | 23 | "Licensor" shall mean the copyright owner or entity authorized by 24 | the copyright owner that is granting the License. 25 | 26 | "Legal Entity" shall mean the union of the acting entity and all 27 | other entities that control, are controlled by, or are under common 28 | control with that entity. For the purposes of this definition, 29 | "control" means (i) the power, direct or indirect, to cause the 30 | direction or management of such entity, whether by contract or 31 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 32 | outstanding shares, or (iii) beneficial ownership of such entity. 33 | 34 | "You" (or "Your") shall mean an individual or Legal Entity 35 | exercising permissions granted by this License. 36 | 37 | "Source" form shall mean the preferred form for making modifications, 38 | including but not limited to software source code, documentation 39 | source, and configuration files. 40 | 41 | "Object" form shall mean any form resulting from mechanical 42 | transformation or translation of a Source form, including but 43 | not limited to compiled object code, generated documentation, 44 | and conversions to other media types. 45 | 46 | "Work" shall mean the work of authorship, whether in Source or 47 | Object form, made available under the License, as indicated by a 48 | copyright notice that is included in or attached to the work 49 | (an example is provided in the Appendix below). 50 | 51 | "Derivative Works" shall mean any work, whether in Source or Object 52 | form, that is based on (or derived from) the Work and for which the 53 | editorial revisions, annotations, elaborations, or other modifications 54 | represent, as a whole, an original work of authorship. For the purposes 55 | of this License, Derivative Works shall not include works that remain 56 | separable from, or merely link (or bind by name) to the interfaces of, 57 | the Work and Derivative Works thereof. 58 | 59 | "Contribution" shall mean any work of authorship, including 60 | the original version of the Work and any modifications or additions 61 | to that Work or Derivative Works thereof, that is intentionally 62 | submitted to Licensor for inclusion in the Work by the copyright owner 63 | or by an individual or Legal Entity authorized to submit on behalf of 64 | the copyright owner. For the purposes of this definition, "submitted" 65 | means any form of electronic, verbal, or written communication sent 66 | to the Licensor or its representatives, including but not limited to 67 | communication on electronic mailing lists, source code control systems, 68 | and issue tracking systems that are managed by, or on behalf of, the 69 | Licensor for the purpose of discussing and improving the Work, but 70 | excluding communication that is conspicuously marked or otherwise 71 | designated in writing by the copyright owner as "Not a Contribution." 72 | 73 | "Contributor" shall mean Licensor and any individual or Legal Entity 74 | on behalf of whom a Contribution has been received by Licensor and 75 | subsequently incorporated within the Work. 76 | 77 | 2. Grant of Copyright License. Subject to the terms and conditions of 78 | this License, each Contributor hereby grants to You a perpetual, 79 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 80 | copyright license to reproduce, prepare Derivative Works of, 81 | publicly display, publicly perform, sublicense, and distribute the 82 | Work and such Derivative Works in Source or Object form. 83 | 84 | 3. Grant of Patent License. Subject to the terms and conditions of 85 | this License, each Contributor hereby grants to You a perpetual, 86 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 87 | (except as stated in this section) patent license to make, have made, 88 | use, offer to sell, sell, import, and otherwise transfer the Work, 89 | where such license applies only to those patent claims licensable 90 | by such Contributor that are necessarily infringed by their 91 | Contribution(s) alone or by combination of their Contribution(s) 92 | with the Work to which such Contribution(s) was submitted. If You 93 | institute patent litigation against any entity (including a 94 | cross-claim or counterclaim in a lawsuit) alleging that the Work 95 | or a Contribution incorporated within the Work constitutes direct 96 | or contributory patent infringement, then any patent licenses 97 | granted to You under this License for that Work shall terminate 98 | as of the date such litigation is filed. 99 | 100 | 4. Redistribution. You may reproduce and distribute copies of the 101 | Work or Derivative Works thereof in any medium, with or without 102 | modifications, and in Source or Object form, provided that You 103 | meet the following conditions: 104 | 105 | (a) You must give any other recipients of the Work or 106 | Derivative Works a copy of this License; and 107 | 108 | (b) You must cause any modified files to carry prominent notices 109 | stating that You changed the files; and 110 | 111 | (c) You must retain, in the Source form of any Derivative Works 112 | that You distribute, all copyright, patent, trademark, and 113 | attribution notices from the Source form of the Work, 114 | excluding those notices that do not pertain to any part of 115 | the Derivative Works; and 116 | 117 | (d) If the Work includes a "NOTICE" text file as part of its 118 | distribution, then any Derivative Works that You distribute must 119 | include a readable copy of the attribution notices contained 120 | within such NOTICE file, excluding those notices that do not 121 | pertain to any part of the Derivative Works, in at least one 122 | of the following places: within a NOTICE text file distributed 123 | as part of the Derivative Works; within the Source form or 124 | documentation, if provided along with the Derivative Works; or, 125 | within a display generated by the Derivative Works, if and 126 | wherever such third-party notices normally appear. The contents 127 | of the NOTICE file are for informational purposes only and 128 | do not modify the License. You may add Your own attribution 129 | notices within Derivative Works that You distribute, alongside 130 | or as an addendum to the NOTICE text from the Work, provided 131 | that such additional attribution notices cannot be construed 132 | as modifying the License. 133 | 134 | You may add Your own copyright statement to Your modifications and 135 | may provide additional or different license terms and conditions 136 | for use, reproduction, or distribution of Your modifications, or 137 | for any such Derivative Works as a whole, provided Your use, 138 | reproduction, and distribution of the Work otherwise complies with 139 | the conditions stated in this License. 140 | 141 | 5. Submission of Contributions. Unless You explicitly state otherwise, 142 | any Contribution intentionally submitted for inclusion in the Work 143 | by You to the Licensor shall be under the terms and conditions of 144 | this License, without any additional terms or conditions. 145 | Notwithstanding the above, nothing herein shall supersede or modify 146 | the terms of any separate license agreement you may have executed 147 | with Licensor regarding such Contributions. 148 | 149 | 6. Trademarks. This License does not grant permission to use the trade 150 | names, trademarks, service marks, or product names of the Licensor, 151 | except as required for reasonable and customary use in describing the 152 | origin of the Work and reproducing the content of the NOTICE file. 153 | 154 | 7. Disclaimer of Warranty. Unless required by applicable law or 155 | agreed to in writing, Licensor provides the Work (and each 156 | Contributor provides its Contributions) on an "AS IS" BASIS, 157 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 158 | implied, including, without limitation, any warranties or conditions 159 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 160 | PARTICULAR PURPOSE. You are solely responsible for determining the 161 | appropriateness of using or redistributing the Work and assume any 162 | risks associated with Your exercise of permissions under this License. 163 | 164 | 8. Limitation of Liability. In no event and under no legal theory, 165 | whether in tort (including negligence), contract, or otherwise, 166 | unless required by applicable law (such as deliberate and grossly 167 | negligent acts) or agreed to in writing, shall any Contributor be 168 | liable to You for damages, including any direct, indirect, special, 169 | incidental, or consequential damages of any character arising as a 170 | result of this License or out of the use or inability to use the 171 | Work (including but not limited to damages for loss of goodwill, 172 | work stoppage, computer failure or malfunction, or any and all 173 | other commercial damages or losses), even if such Contributor 174 | has been advised of the possibility of such damages. 175 | 176 | 9. Accepting Warranty or Additional Liability. While redistributing 177 | the Work or Derivative Works thereof, You may choose to offer, 178 | and charge a fee for, acceptance of support, warranty, indemnity, 179 | or other liability obligations and/or rights consistent with this 180 | License. However, in accepting such obligations, You may act only 181 | on Your own behalf and on Your sole responsibility, not on behalf 182 | of any other Contributor, and only if You agree to indemnify, 183 | defend, and hold each Contributor harmless for any liability 184 | incurred by, or claims asserted against, such Contributor by reason 185 | of your accepting any such warranty or additional liability. 186 | - sources: README.md 187 | text: Cobra is released under the Apache 2.0 license. See [LICENSE.txt](LICENSE.txt) 188 | notices: [] 189 | -------------------------------------------------------------------------------- /msgpackrpc/connection.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package msgpackrpc 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "io" 22 | "sync" 23 | "sync/atomic" 24 | "time" 25 | 26 | "github.com/vmihailenco/msgpack/v5" 27 | ) 28 | 29 | type MessageID uint 30 | 31 | const ( 32 | messageTypeRequest = 0 33 | messageTypeResponse = 1 34 | messageTypeNotification = 2 35 | ) 36 | 37 | // Connection is a MessagePack-RPC connection 38 | type Connection struct { 39 | in io.ReadCloser 40 | out io.WriteCloser 41 | outEncoder *msgpack.Encoder 42 | outMutex sync.Mutex 43 | errorHandler ErrorHandler 44 | requestHandler RequestHandler 45 | notificationHandler NotificationHandler 46 | logger Logger 47 | loggerMutex sync.Mutex 48 | 49 | activeInRequests map[MessageID]*inRequest 50 | activeInRequestsMutex sync.Mutex 51 | 52 | activeOutRequests map[MessageID]*outRequest 53 | activeOutRequestsMutex sync.Mutex 54 | lastOutRequestsIndex atomic.Uint32 55 | 56 | workerSlots chan token 57 | } 58 | 59 | type token struct{} 60 | 61 | type inRequest struct { 62 | cancel func() 63 | } 64 | 65 | type outRequest struct { 66 | resultChan chan<- *outResponse 67 | method string 68 | } 69 | 70 | type outResponse struct { 71 | reqError any 72 | reqResult any 73 | } 74 | 75 | // RequestHandler handles requests from a MessagePack-RPC Connection. 76 | type RequestHandler func(ctx context.Context, logger FunctionLogger, method string, params []any) (result any, err any) 77 | 78 | // NotificationHandler handles notifications from a MessagePack-RPC Connection. 79 | type NotificationHandler func(logger FunctionLogger, method string, params []any) 80 | 81 | // ErrorHandler handles errors from a MessagePack-RPC Connection. 82 | // It is called when an error occurs while reading from the connection or when 83 | // sending a request or notification. 84 | type ErrorHandler func(error) 85 | 86 | // NewConnection creates a new MessagePack-RPC Connection handler. 87 | func NewConnection(in io.ReadCloser, out io.WriteCloser, requestHandler RequestHandler, notificationHandler NotificationHandler, errorHandler ErrorHandler) *Connection { 88 | return NewConnectionWithMaxWorkers(in, out, requestHandler, notificationHandler, errorHandler, 0) 89 | } 90 | 91 | // NewConnectionWithMaxWorkers creates a new MessagePack-RPC Connection handler 92 | // with a specified maximum number of worker goroutines to handle incoming requests. 93 | func NewConnectionWithMaxWorkers(in io.ReadCloser, out io.WriteCloser, requestHandler RequestHandler, notificationHandler NotificationHandler, errorHandler ErrorHandler, maxWorkers int) *Connection { 94 | outEncoder := msgpack.NewEncoder(out) 95 | outEncoder.UseCompactInts(true) 96 | if requestHandler == nil { 97 | requestHandler = func(ctx context.Context, logger FunctionLogger, method string, params []any) (result any, err any) { 98 | return nil, fmt.Errorf("method not implemented: %s", method) 99 | } 100 | } 101 | if notificationHandler == nil { 102 | notificationHandler = func(logger FunctionLogger, method string, params []any) { 103 | // ignore notifications 104 | } 105 | } 106 | if errorHandler == nil { 107 | errorHandler = func(err error) { 108 | // ignore errors 109 | } 110 | } 111 | conn := &Connection{ 112 | in: in, 113 | out: out, 114 | outEncoder: outEncoder, 115 | requestHandler: requestHandler, 116 | notificationHandler: notificationHandler, 117 | errorHandler: errorHandler, 118 | activeInRequests: map[MessageID]*inRequest{}, 119 | activeOutRequests: map[MessageID]*outRequest{}, 120 | logger: NullLogger{}, 121 | } 122 | if maxWorkers > 0 { 123 | conn.workerSlots = make(chan token, maxWorkers) 124 | } 125 | return conn 126 | } 127 | 128 | func (c *Connection) startWorker(cb func()) { 129 | if c.workerSlots == nil { 130 | go cb() 131 | return 132 | } 133 | c.workerSlots <- token{} 134 | go func() { 135 | defer func() { <-c.workerSlots }() 136 | cb() 137 | }() 138 | } 139 | 140 | func (c *Connection) SetLogger(l Logger) { 141 | c.loggerMutex.Lock() 142 | c.logger = l 143 | c.loggerMutex.Unlock() 144 | } 145 | 146 | func (c *Connection) Run() { 147 | in := msgpack.NewDecoder(c.in) 148 | for { 149 | var data []any 150 | start := time.Now() 151 | if v, err := in.DecodeInterface(); err != nil { 152 | c.errorHandler(fmt.Errorf("can't read packet: %w", err)) 153 | return // unrecoverable 154 | } else if s, ok := v.([]any); !ok { 155 | c.errorHandler(fmt.Errorf("invalid packet, expected array, got: %T", v)) 156 | continue // ignore invalid packets 157 | } else { 158 | data = s 159 | } 160 | elapsed := time.Since(start) 161 | c.loggerMutex.Lock() 162 | c.logger.LogIncomingDataDelay(elapsed) 163 | c.loggerMutex.Unlock() 164 | 165 | if err := c.processIncomingMessage(data); err != nil { 166 | c.errorHandler(err) 167 | } 168 | } 169 | } 170 | 171 | func (c *Connection) processIncomingMessage(data []any) error { 172 | if len(data) < 3 { 173 | return fmt.Errorf("invalid packet, expected array with at least 3 elements") 174 | } 175 | 176 | msgType, ok := ToInt(data[0]) 177 | if !ok { 178 | return fmt.Errorf("invalid packet, expected int as first element, got %T", data[0]) 179 | } 180 | 181 | switch msgType { 182 | case messageTypeRequest: 183 | if len(data) != 4 { 184 | return fmt.Errorf("invalid request, expected array with 4 elements") 185 | } 186 | if id, ok := ToUint(data[1]); !ok { 187 | return fmt.Errorf("invalid request, expected msgid (uint) as second element") 188 | } else if method, ok := data[2].(string); !ok { 189 | return fmt.Errorf("invalid request, expected method (string) as third element") 190 | } else if params, ok := data[3].([]any); !ok { 191 | return fmt.Errorf("invalid request, expected params (array) as fourth element") 192 | } else { 193 | c.handleIncomingRequest(MessageID(id), method, params) 194 | } 195 | return nil 196 | case messageTypeResponse: 197 | if len(data) != 4 { 198 | return fmt.Errorf("invalid response, expected array with 4 elements") 199 | } 200 | if id, ok := ToUint(data[1]); !ok { 201 | return fmt.Errorf("invalid response, expected msgid (uint) as second element") 202 | } else { 203 | reqError := data[2] 204 | reqResult := data[3] 205 | c.handleIncomingResponse(MessageID(id), reqError, reqResult) 206 | } 207 | return nil 208 | case messageTypeNotification: 209 | if len(data) != 3 { 210 | return fmt.Errorf("invalid notification, expected array with 3 elements") 211 | } 212 | if method, ok := data[1].(string); !ok { 213 | return fmt.Errorf("invalid notification, expected method (string) as second element") 214 | } else if params, ok := data[2].([]any); !ok { 215 | return fmt.Errorf("invalid notification, expected params (array) as third element") 216 | } else { 217 | c.handleIncomingNotification(method, params) 218 | } 219 | return nil 220 | default: 221 | return fmt.Errorf("invalid packet, expected request, response or notification") 222 | } 223 | } 224 | 225 | func (c *Connection) handleIncomingRequest(id MessageID, method string, params []any) { 226 | ctx, cancel := context.WithCancel(context.Background()) 227 | req := &inRequest{cancel: cancel} 228 | 229 | c.activeInRequestsMutex.Lock() 230 | if overriddenReq := c.activeInRequests[id]; overriddenReq != nil { 231 | // RPC protocol violation: there is already an active request with the same ID. 232 | // Cancel the existing request and replace it with the new one 233 | overriddenReq.cancel() 234 | c.errorHandler(fmt.Errorf("RPC protocol violation: request with ID %v already active, canceling it", id)) 235 | } 236 | c.activeInRequests[id] = req 237 | c.activeInRequestsMutex.Unlock() 238 | 239 | c.loggerMutex.Lock() 240 | logger := c.logger.LogIncomingRequest(id, method, params) 241 | c.loggerMutex.Unlock() 242 | 243 | c.startWorker(func() { 244 | reqResult, reqError := c.requestHandler(ctx, logger, method, params) 245 | 246 | var existing *inRequest 247 | c.activeInRequestsMutex.Lock() 248 | existing = c.activeInRequests[id] 249 | if existing == req { 250 | existing.cancel() 251 | delete(c.activeInRequests, id) 252 | } 253 | c.activeInRequestsMutex.Unlock() 254 | if existing != req { 255 | return 256 | } 257 | 258 | c.loggerMutex.Lock() 259 | c.logger.LogOutgoingResponse(id, method, reqResult, reqError) 260 | c.loggerMutex.Unlock() 261 | 262 | if err := c.send(messageTypeResponse, id, reqError, reqResult); err != nil { 263 | c.errorHandler(fmt.Errorf("error sending response: %w", err)) 264 | c.Close() 265 | } 266 | }) 267 | } 268 | 269 | func (c *Connection) handleIncomingNotification(method string, params []any) { 270 | if method == "$/cancelRequest" { 271 | // Send cancelation signal and exit 272 | if len(params) != 1 { 273 | c.errorHandler(fmt.Errorf("invalid cancelRequest, expected array with 1 element")) 274 | return 275 | } 276 | id, ok := ToUint(params[0]) 277 | if !ok { 278 | c.errorHandler(fmt.Errorf("invalid cancelRequest, expected msgid (uint) as first element")) 279 | return 280 | } 281 | c.cancelIncomingRequest(MessageID(id)) 282 | return 283 | } 284 | 285 | c.loggerMutex.Lock() 286 | logger := c.logger.LogIncomingNotification(method, params) 287 | c.loggerMutex.Unlock() 288 | 289 | c.startWorker(func() { 290 | c.notificationHandler(logger, method, params) 291 | }) 292 | } 293 | 294 | func (c *Connection) handleIncomingResponse(id MessageID, reqError any, reqResult any) { 295 | c.activeOutRequestsMutex.Lock() 296 | req, ok := c.activeOutRequests[id] 297 | if ok { 298 | delete(c.activeOutRequests, id) 299 | } 300 | c.activeOutRequestsMutex.Unlock() 301 | 302 | if !ok { 303 | c.errorHandler(fmt.Errorf("invalid ID in request response '%v': double answer or request not sent", id)) 304 | return 305 | } 306 | 307 | req.resultChan <- &outResponse{ 308 | reqError: reqError, 309 | reqResult: reqResult, 310 | } 311 | } 312 | 313 | func (c *Connection) cancelIncomingRequest(id MessageID) { 314 | c.activeInRequestsMutex.Lock() 315 | if req, ok := c.activeInRequests[id]; ok { 316 | c.loggerMutex.Lock() 317 | c.logger.LogIncomingCancelRequest(id) 318 | c.loggerMutex.Unlock() 319 | 320 | req.cancel() 321 | } 322 | c.activeInRequestsMutex.Unlock() 323 | } 324 | 325 | func (c *Connection) Close() { 326 | _ = c.in.Close() 327 | _ = c.out.Close() 328 | } 329 | 330 | func (c *Connection) SendRequest(ctx context.Context, method string, params ...any) (reqResult any, reqError any, err error) { 331 | if params == nil { 332 | params = []any{} 333 | } 334 | id := MessageID(c.lastOutRequestsIndex.Add(1)) 335 | 336 | c.loggerMutex.Lock() 337 | c.logger.LogOutgoingRequest(id, method, params) 338 | c.loggerMutex.Unlock() 339 | 340 | resultChan := make(chan *outResponse, 1) 341 | c.activeOutRequestsMutex.Lock() 342 | c.activeOutRequests[id] = &outRequest{ 343 | resultChan: resultChan, 344 | method: method, 345 | } 346 | c.activeOutRequestsMutex.Unlock() 347 | 348 | if err := c.send(messageTypeRequest, id, method, params); err != nil { 349 | c.activeOutRequestsMutex.Lock() 350 | delete(c.activeOutRequests, id) 351 | c.activeOutRequestsMutex.Unlock() 352 | return nil, nil, fmt.Errorf("sending request: %w", err) 353 | } 354 | 355 | // Wait the response or send cancel request if requested from context 356 | var result *outResponse 357 | select { 358 | case result = <-resultChan: 359 | // got result, do nothing 360 | 361 | case <-ctx.Done(): 362 | c.activeOutRequestsMutex.Lock() 363 | _, active := c.activeOutRequests[id] 364 | c.activeOutRequestsMutex.Unlock() 365 | if active { 366 | c.loggerMutex.Lock() 367 | c.logger.LogOutgoingCancelRequest(id) 368 | c.loggerMutex.Unlock() 369 | 370 | _ = c.SendNotification("$/cancelRequest", id) // ignore error (it won't matter anyway) 371 | } 372 | 373 | // After cancelation wait for result... 374 | result = <-resultChan 375 | } 376 | 377 | c.loggerMutex.Lock() 378 | c.logger.LogIncomingResponse(id, method, result.reqResult, result.reqError) 379 | c.loggerMutex.Unlock() 380 | 381 | return result.reqResult, result.reqError, nil 382 | } 383 | 384 | func (c *Connection) SendNotification(method string, params ...any) error { 385 | if params == nil { 386 | params = []any{} 387 | } 388 | 389 | c.loggerMutex.Lock() 390 | c.logger.LogOutgoingNotification(method, params) 391 | c.loggerMutex.Unlock() 392 | 393 | if err := c.send(messageTypeNotification, method, params); err != nil { 394 | return fmt.Errorf("sending notification: %w", err) 395 | } 396 | return nil 397 | } 398 | 399 | func (c *Connection) send(data ...any) error { 400 | start := time.Now() 401 | 402 | c.outMutex.Lock() 403 | err := c.outEncoder.Encode(data) 404 | c.outMutex.Unlock() 405 | if err != nil { 406 | return err 407 | } 408 | 409 | elapsed := time.Since(start) 410 | 411 | c.loggerMutex.Lock() 412 | c.logger.LogOutgoingDataDelay(elapsed) 413 | c.loggerMutex.Unlock() 414 | return nil 415 | } 416 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Ladicle/tabwriter v1.0.0 h1:DZQqPvMumBDwVNElso13afjYLNp0Z7pHqHnu0r4t9Dg= 4 | github.com/Ladicle/tabwriter v1.0.0/go.mod h1:c4MdCjxQyTbGuQO/gvqJ+IA/89UEwrsD6hUCW98dyp4= 5 | github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= 6 | github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= 7 | github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= 8 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 9 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 10 | github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= 11 | github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= 12 | github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0= 13 | github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= 14 | github.com/alecthomas/chroma/v2 v2.19.0 h1:Im+SLRgT8maArxv81mULDWN8oKxkzboH07CHesxElq4= 15 | github.com/alecthomas/chroma/v2 v2.19.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3aWCNE14TFlepBk= 16 | github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= 17 | github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= 18 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= 19 | github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= 20 | github.com/arduino/go-paths-helper v1.14.0 h1:b4C8KJa7CNz2XnzTWg97M3LAmzWSWrj+m/o5/skzv3Y= 21 | github.com/arduino/go-paths-helper v1.14.0/go.mod h1:dDodKn2ZX4iwuoBMapdDO+5d0oDLBeM4BS0xS4i40Ak= 22 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= 23 | github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= 24 | github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ= 25 | github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o= 26 | github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= 27 | github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= 28 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 29 | github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= 30 | github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= 31 | github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= 32 | github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= 33 | github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= 34 | github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= 35 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 36 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 37 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/djherbis/buffer v1.1.0/go.mod h1:VwN8VdFkMY0DCALdY8o00d3IZ6Amz/UNVMWcSaJT44o= 39 | github.com/djherbis/buffer v1.2.0 h1:PH5Dd2ss0C7CRRhQCZ2u7MssF+No9ide8Ye71nPHcrQ= 40 | github.com/djherbis/buffer v1.2.0/go.mod h1:fjnebbZjCUpPinBRD+TDwXSOeNQ7fPQWLfGQqiAiUyE= 41 | github.com/djherbis/nio/v3 v3.0.1 h1:6wxhnuppteMa6RHA4L81Dq7ThkZH8SwnDzXDYy95vB4= 42 | github.com/djherbis/nio/v3 v3.0.1/go.mod h1:Ng4h80pbZFMla1yKzm61cF0tqqilXZYrogmWgZxOcmg= 43 | github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= 44 | github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= 45 | github.com/dominikbraun/graph v0.23.0 h1:TdZB4pPqCLFxYhdyMFb1TBdFxp8XLcJfTTBQucVPgCo= 46 | github.com/dominikbraun/graph v0.23.0/go.mod h1:yOjYyogZLY1LSG9E33JWZJiq5k83Qy2C6POAuiViluc= 47 | github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= 48 | github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= 49 | github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= 50 | github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= 51 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 52 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 53 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 54 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 55 | github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= 56 | github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= 57 | github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= 58 | github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= 59 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 60 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 61 | github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= 62 | github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= 63 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= 64 | github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= 65 | github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= 66 | github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= 67 | github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= 68 | github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= 69 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 70 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 71 | github.com/go-task/task/v3 v3.44.1 h1:i+lP9Ng3SQVUdBR8mmuiID7uKoIu+5LwwkOjio92VEQ= 72 | github.com/go-task/task/v3 v3.44.1/go.mod h1:mEX7XBXFUGlbIu8Aqv3r8N5LYYdHJ7PTm+Ak6bSuIvo= 73 | github.com/go-task/template v0.2.0 h1:xW7ek0o65FUSTbKcSNeg2Vyf/I7wYXFgLUznptvviBE= 74 | github.com/go-task/template v0.2.0/go.mod h1:dbdoUb6qKnHQi1y6o+IdIrs0J4o/SEhSTA6bbzZmdtc= 75 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= 76 | github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= 77 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 78 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 79 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 80 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 81 | github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= 82 | github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= 83 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 84 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 85 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 86 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 87 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 88 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 89 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 90 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 91 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 92 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 93 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 94 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 95 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 96 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 97 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 98 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 99 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 100 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 101 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 102 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 103 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 104 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 105 | github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= 106 | github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= 107 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 108 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 109 | github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= 110 | github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= 111 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 112 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 113 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 114 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 115 | github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg= 116 | github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA= 117 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 118 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 119 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 120 | github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY= 121 | github.com/sajari/fuzzy v1.0.0/go.mod h1:OjYR6KxoWOe9+dOlXeiCJd4dIbED4Oo8wpS89o0pwOo= 122 | github.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E= 123 | github.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= 124 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= 125 | github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= 126 | github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 127 | github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= 128 | github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= 129 | github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= 130 | github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= 131 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 132 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 133 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 134 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 135 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 136 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 137 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 138 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 139 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 140 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 141 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 142 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 143 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 144 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 145 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 146 | github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= 147 | github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= 148 | github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= 149 | github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= 150 | go.bug.st/f v0.4.0 h1:Vstqb950nMA+PhAlRxUw8QL1ntHy/gXHNyyzjkQLJ10= 151 | go.bug.st/f v0.4.0/go.mod h1:bMo23205ll7UW63KwO1ut5RdlJ9JK8RyEEr88CmOF5Y= 152 | go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= 153 | go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= 154 | golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 155 | golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= 156 | golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 157 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8= 158 | golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY= 159 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 160 | golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= 161 | golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= 162 | golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= 163 | golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 164 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 165 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 169 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 170 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 171 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 172 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 173 | golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= 174 | golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 175 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 176 | golang.org/x/term v0.33.0 h1:NuFncQrRcaRvVmgRkvM3j/F00gWIAlcmlB8ACEKmGIg= 177 | golang.org/x/term v0.33.0/go.mod h1:s18+ql9tYWp1IfpV9DmCtQDDSRBUjKaw9M1eAv5UeF0= 178 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 179 | golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= 180 | golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 181 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 182 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 183 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 184 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 185 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 186 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 187 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 188 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 189 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 190 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 191 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 192 | mvdan.cc/sh/v3 v3.12.0 h1:ejKUR7ONP5bb+UGHGEG/k9V5+pRVIyD+LsZz7o8KHrI= 193 | mvdan.cc/sh/v3 v3.12.0/go.mod h1:Se6Cj17eYSn+sNooLZiEUnNNmNxg0imoYlTu4CyaGyg= 194 | -------------------------------------------------------------------------------- /internal/network-api/network-api_test.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package networkapi 17 | 18 | import ( 19 | "fmt" 20 | "sync" 21 | "testing" 22 | "time" 23 | 24 | "github.com/arduino/arduino-router/msgpackrpc" 25 | 26 | "github.com/stretchr/testify/require" 27 | ) 28 | 29 | const testCert = "-----BEGIN CERTIFICATE-----\n" + 30 | "MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF\n" + 31 | "ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n" + 32 | "b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL\n" + 33 | "MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n" + 34 | "b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj\n" + 35 | "ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM\n" + 36 | "9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw\n" + 37 | "IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6\n" + 38 | "VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L\n" + 39 | "93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm\n" + 40 | "jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC\n" + 41 | "AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA\n" + 42 | "A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI\n" + 43 | "U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs\n" + 44 | "N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv\n" + 45 | "o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU\n" + 46 | "5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy\n" + 47 | "rqXRfboQnoZsG4q5WTP468SQvvG5\n" + 48 | "-----END CERTIFICATE-----\n" + 49 | /* https://www.amazontrust.com/repository/AmazonRootCA2.pem */ 50 | "-----BEGIN CERTIFICATE-----\n" + 51 | "MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF\n" + 52 | "ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6\n" + 53 | "b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL\n" + 54 | "MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv\n" + 55 | "b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK\n" + 56 | "gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ\n" + 57 | "W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg\n" + 58 | "1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K\n" + 59 | "8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r\n" + 60 | "2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me\n" + 61 | "z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR\n" + 62 | "8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj\n" + 63 | "mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz\n" + 64 | "7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6\n" + 65 | "+XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI\n" + 66 | "0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB\n" + 67 | "Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm\n" + 68 | "UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2\n" + 69 | "LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY\n" + 70 | "+gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS\n" + 71 | "k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl\n" + 72 | "7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm\n" + 73 | "btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl\n" + 74 | "urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+\n" + 75 | "fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63\n" + 76 | "n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE\n" + 77 | "76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H\n" + 78 | "9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT\n" + 79 | "4PsJYGw=\n" + 80 | "-----END CERTIFICATE-----\n" + 81 | /* https://www.amazontrust.com/repository/AmazonRootCA3.pem */ 82 | "-----BEGIN CERTIFICATE-----\n" + 83 | "MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5\n" + 84 | "MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\n" + 85 | "Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\n" + 86 | "A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\n" + 87 | "Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl\n" + 88 | "ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j\n" + 89 | "QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr\n" + 90 | "ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr\n" + 91 | "BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM\n" + 92 | "YyRIHN8wfdVoOw==\n" + 93 | "-----END CERTIFICATE-----\n" + 94 | /* https://www.amazontrust.com/repository/AmazonRootCA4.pem */ 95 | "-----BEGIN CERTIFICATE-----\n" + 96 | "MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5\n" + 97 | "MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g\n" + 98 | "Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG\n" + 99 | "A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg\n" + 100 | "Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi\n" + 101 | "9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk\n" + 102 | "M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB\n" + 103 | "/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB\n" + 104 | "MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw\n" + 105 | "CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW\n" + 106 | "1KyLa2tJElMzrdfkviT8tQp21KW8EA==\n" + 107 | "-----END CERTIFICATE-----\n" + 108 | /* https://www.amazontrust.com/repository/SFSRootCAG2.pem */ 109 | "-----BEGIN CERTIFICATE-----\n" + 110 | "MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx\n" + 111 | "EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT\n" + 112 | "HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs\n" + 113 | "ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5\n" + 114 | "MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD\n" + 115 | "VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy\n" + 116 | "ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy\n" + 117 | "dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI\n" + 118 | "hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p\n" + 119 | "OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2\n" + 120 | "8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K\n" + 121 | "Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe\n" + 122 | "hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk\n" + 123 | "6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw\n" + 124 | "DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q\n" + 125 | "AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI\n" + 126 | "bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB\n" + 127 | "ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z\n" + 128 | "qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd\n" + 129 | "iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn\n" + 130 | "0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN\n" + 131 | "sSi6\n" + 132 | "-----END CERTIFICATE-----\n" + 133 | /* iot.arduino.cc:8885 */ 134 | "-----BEGIN CERTIFICATE-----\n" + 135 | "MIIB0DCCAXagAwIBAgIUb62eK/Vv1baaPAaY5DADBUbxB1owCgYIKoZIzj0EAwIw\n" + 136 | "RTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkFyZHVpbm8gTExDIFVTMQswCQYDVQQL\n" + 137 | "EwJJVDEQMA4GA1UEAxMHQXJkdWlubzAgFw0yNTAxMTAxMDUzMjJaGA8yMDU1MDEw\n" + 138 | "MzEwNTMyMlowRTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkFyZHVpbm8gTExDIFVT\n" + 139 | "MQswCQYDVQQLEwJJVDEQMA4GA1UEAxMHQXJkdWlubzBZMBMGByqGSM49AgEGCCqG\n" + 140 | "SM49AwEHA0IABKHhU2w1UhozDegrrFsSwY9QN7M+ZJug7icCNceNWhBF0Mr1UuyX\n" + 141 | "8pr/gcbieZc/0znG16HMa2GFcPY7rmIdccijQjBAMA8GA1UdEwEB/wQFMAMBAf8w\n" + 142 | "DgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRCZSmE0ASI0cYD9AmzeOM7EijgPjAK\n" + 143 | "BggqhkjOPQQDAgNIADBFAiEAz6TLYP9eiVOr/cVU/11zwGofe/FoNe4p1BlzMl7G\n" + 144 | "VVACIG8tL3Ta2WbIOaUVpBL2gfLuI9WSW1sR++zXP+zFhmen\n" + 145 | "-----END CERTIFICATE-----\n" + 146 | /* staging certificate */ 147 | "-----BEGIN CERTIFICATE-----\n" + 148 | "MIIBzzCCAXagAwIBAgIUI5fEitwlnwujc/mU0d8LnDiDXBIwCgYIKoZIzj0EAwIw\n" + 149 | "RTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkFyZHVpbm8gTExDIFVTMQswCQYDVQQL\n" + 150 | "EwJJVDEQMA4GA1UEAxMHQXJkdWlubzAgFw0yNTAxMDgxMTA4MzdaGA8yMDU1MDEw\n" + 151 | "MTExMDgzN1owRTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkFyZHVpbm8gTExDIFVT\n" + 152 | "MQswCQYDVQQLEwJJVDEQMA4GA1UEAxMHQXJkdWlubzBZMBMGByqGSM49AgEGCCqG\n" + 153 | "SM49AwEHA0IABBFwNODDPgC9C1kDmKBbawtQ31FmTudAXVpGSOUwcDX582z820cD\n" + 154 | "eIaCwOxghmI+p/CpOH63f5F6h23ErqZMBkijQjBAMA8GA1UdEwEB/wQFMAMBAf8w\n" + 155 | "DgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBQdnBmQGLB7ls/r1Tetdp+MVMqxfTAK\n" + 156 | "BggqhkjOPQQDAgNHADBEAiBPSZ9HpF7MuFoK4Jsz//PHILQuHM4WmRopQR9ysSs0\n" + 157 | "HAIgNadMPgxv01dy59kCgzehgKzmKdTF0rG1SniYqnkLqPA=\n" + 158 | "-----END CERTIFICATE-----\n" 159 | 160 | func TestTCPNetworkAPI(t *testing.T) { 161 | ctx := t.Context() 162 | var rpc *msgpackrpc.Connection 163 | listID, err := tcpListen(ctx, rpc, []any{"localhost", 9999}) 164 | require.Nil(t, err) 165 | require.Equal(t, uint(1), listID) 166 | 167 | var wg sync.WaitGroup 168 | wg.Go(func() { 169 | connID, err := tcpConnect(ctx, rpc, []any{"localhost", uint16(9999)}) 170 | require.Nil(t, err) 171 | 172 | n, err := tcpWrite(ctx, rpc, []any{connID, []byte("Hello")}) 173 | require.Nil(t, err) 174 | require.Equal(t, 5, n) 175 | 176 | res, err := tcpClose(ctx, rpc, []any{connID}) 177 | require.Nil(t, err) 178 | require.Equal(t, "", res) 179 | 180 | res, err = tcpClose(ctx, rpc, []any{connID}) 181 | require.Equal(t, []any{2, fmt.Sprintf("Connection not found for ID: %d", connID)}, err) 182 | require.Nil(t, res) 183 | }) 184 | 185 | connID, err := tcpAccept(ctx, rpc, []any{listID}) 186 | require.Nil(t, err) 187 | 188 | buff, err := tcpRead(ctx, rpc, []any{connID, 3}) 189 | require.Nil(t, err) 190 | require.Equal(t, []byte("Hel"), buff) 191 | 192 | buff, err = tcpRead(ctx, rpc, []any{connID, 3}) 193 | require.Nil(t, err) 194 | require.Equal(t, []byte("lo"), buff) 195 | 196 | buff, err = tcpRead(ctx, rpc, []any{connID, 3}) 197 | require.Equal(t, []any{3, "Failed to read from connection: EOF"}, err) 198 | require.Nil(t, buff) 199 | 200 | res, err := tcpCloseListener(ctx, rpc, []any{connID}) 201 | require.Equal(t, []any{2, fmt.Sprintf("Listener not found for ID: %d", connID)}, err) 202 | require.Nil(t, res) 203 | 204 | res, err = tcpClose(ctx, rpc, []any{connID}) 205 | require.Nil(t, err) 206 | require.Equal(t, "", res) 207 | 208 | res, err = tcpClose(ctx, rpc, []any{listID}) 209 | require.Equal(t, []any{2, fmt.Sprintf("Connection not found for ID: %d", listID)}, err) 210 | require.Nil(t, res) 211 | 212 | res, err = tcpCloseListener(ctx, rpc, []any{listID}) 213 | require.Nil(t, err) 214 | require.Equal(t, "", res) 215 | 216 | res, err = tcpClose(ctx, rpc, []any{listID}) 217 | require.Equal(t, []any{2, fmt.Sprintf("Connection not found for ID: %d", listID)}, err) 218 | require.Nil(t, res) 219 | 220 | res, err = tcpCloseListener(ctx, rpc, []any{listID}) 221 | require.Equal(t, []any{2, fmt.Sprintf("Listener not found for ID: %d", listID)}, err) 222 | require.Nil(t, res) 223 | 224 | // Test SSL connection 225 | connIDSSL, err := tcpConnectSSL(ctx, rpc, []any{"www.arduino.cc", uint16(443)}) 226 | require.Nil(t, err) 227 | require.Equal(t, uint(4), connIDSSL) 228 | 229 | res, err = tcpClose(ctx, rpc, []any{connIDSSL}) 230 | require.Nil(t, err) 231 | require.Equal(t, "", res) 232 | 233 | // Test SSL connection with failing certificate verification 234 | connIDSSL, err = tcpConnectSSL(ctx, rpc, []any{"www.arduino.cc", uint16(443), testCert}) 235 | require.Equal(t, []any{2, "Failed to connect to server: tls: failed to verify certificate: x509: certificate signed by unknown authority"}, err) 236 | require.Nil(t, connIDSSL) 237 | 238 | wg.Wait() 239 | } 240 | 241 | func TestUDPNetworkAPI(t *testing.T) { 242 | ctx := t.Context() 243 | conn1, err := udpConnect(ctx, nil, []any{"0.0.0.0", 9800}) 244 | require.Nil(t, err) 245 | 246 | conn2, err := udpConnect(ctx, nil, []any{"0.0.0.0", 9900}) 247 | require.Nil(t, err) 248 | require.NotEqual(t, conn1, conn2) 249 | 250 | { 251 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9900}) 252 | require.Nil(t, err) 253 | require.True(t, res.(bool)) 254 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("Hello")}) 255 | require.Nil(t, err) 256 | require.Equal(t, 5, res) 257 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 258 | require.Nil(t, err) 259 | require.Equal(t, 5, res) 260 | } 261 | { 262 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 263 | require.Nil(t, err) 264 | require.Equal(t, []any{5, "127.0.0.1", 9800}, res) 265 | 266 | res2, err := udpRead(ctx, nil, []any{conn2, 100}) 267 | require.Nil(t, err) 268 | require.Equal(t, []uint8("Hello"), res2) 269 | } 270 | { 271 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9900}) 272 | require.Nil(t, err) 273 | require.True(t, res.(bool)) 274 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("On")}) 275 | require.Nil(t, err) 276 | require.Equal(t, 2, res) 277 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("e")}) 278 | require.Nil(t, err) 279 | require.Equal(t, 1, res) 280 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 281 | require.Nil(t, err) 282 | require.Equal(t, 3, res) 283 | } 284 | { 285 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9900}) 286 | require.Nil(t, err) 287 | require.True(t, res.(bool)) 288 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("Two")}) 289 | require.Nil(t, err) 290 | require.Equal(t, 3, res) 291 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 292 | require.Nil(t, err) 293 | require.Equal(t, 3, res) 294 | } 295 | { 296 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 297 | require.Nil(t, err) 298 | require.Equal(t, []any{3, "127.0.0.1", 9800}, res) 299 | 300 | // A partial read of a packet is allowed 301 | res2, err := udpRead(ctx, nil, []any{conn2, 2}) 302 | require.Nil(t, err) 303 | require.Equal(t, []uint8("On"), res2) 304 | } 305 | { 306 | // Even if the previous packet was only partially read, 307 | // the next packet can be received 308 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 309 | require.Nil(t, err) 310 | require.Equal(t, []any{3, "127.0.0.1", 9800}, res) 311 | 312 | res2, err := udpRead(ctx, nil, []any{conn2, 100}) 313 | require.Nil(t, err) 314 | require.Equal(t, []uint8("Two"), res2) 315 | } 316 | { 317 | res, err := udpClose(ctx, nil, []any{conn1}) 318 | require.Nil(t, err) 319 | require.Equal(t, "", res) 320 | } 321 | { 322 | res, err := udpClose(ctx, nil, []any{conn2}) 323 | require.Nil(t, err) 324 | require.Equal(t, "", res) 325 | } 326 | } 327 | 328 | func TestUDPNetworkUnboundClientAPI(t *testing.T) { 329 | ctx := t.Context() 330 | conn1, err := udpConnect(ctx, nil, []any{"", 0}) 331 | require.Nil(t, err) 332 | 333 | conn2, err := udpConnect(ctx, nil, []any{"0.0.0.0", 9901}) 334 | require.Nil(t, err) 335 | require.NotEqual(t, conn1, conn2) 336 | 337 | { 338 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9901}) 339 | require.Nil(t, err) 340 | require.True(t, res.(bool)) 341 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("Hello")}) 342 | require.Nil(t, err) 343 | require.Equal(t, 5, res) 344 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 345 | require.Nil(t, err) 346 | require.Equal(t, 5, res) 347 | } 348 | { 349 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 350 | require.Nil(t, err) 351 | require.Equal(t, 5, res.([]any)[0]) 352 | 353 | res2, err := udpRead(ctx, nil, []any{conn2, 2}) 354 | require.Nil(t, err) 355 | require.Equal(t, []uint8("He"), res2) 356 | 357 | res2, err = udpRead(ctx, nil, []any{conn2, 20}) 358 | require.Nil(t, err) 359 | require.Equal(t, []uint8("llo"), res2) 360 | } 361 | { 362 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9901}) 363 | require.Nil(t, err) 364 | require.True(t, res.(bool)) 365 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("One")}) 366 | require.Nil(t, err) 367 | require.Equal(t, 3, res) 368 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 369 | require.Nil(t, err) 370 | require.Equal(t, 3, res) 371 | } 372 | { 373 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9901}) 374 | require.Nil(t, err) 375 | require.True(t, res.(bool)) 376 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("Two")}) 377 | require.Nil(t, err) 378 | require.Equal(t, 3, res) 379 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 380 | require.Nil(t, err) 381 | require.Equal(t, 3, res) 382 | } 383 | { 384 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 385 | require.Nil(t, err) 386 | require.Equal(t, 3, res.([]any)[0]) 387 | 388 | res2, err := udpRead(ctx, nil, []any{conn2, 100}) 389 | require.Nil(t, err) 390 | require.Equal(t, []uint8("One"), res2) 391 | } 392 | { 393 | res, err := udpAwaitPacket(ctx, nil, []any{conn2}) 394 | require.Nil(t, err) 395 | require.Equal(t, 3, res.([]any)[0]) 396 | 397 | res2, err := udpRead(ctx, nil, []any{conn2, 100}) 398 | require.Nil(t, err) 399 | require.Equal(t, []uint8("Two"), res2) 400 | } 401 | 402 | // Check timeouts 403 | go func() { 404 | time.Sleep(200 * time.Millisecond) 405 | res, err := udpBeginPacket(ctx, nil, []any{conn1, "127.0.0.1", 9901}) 406 | require.Nil(t, err) 407 | require.True(t, res.(bool)) 408 | res, err = udpWrite(ctx, nil, []any{conn1, []byte("Three")}) 409 | require.Nil(t, err) 410 | require.Equal(t, 5, res) 411 | res, err = udpEndPacket(ctx, nil, []any{conn1}) 412 | require.Nil(t, err) 413 | require.Equal(t, 5, res) 414 | }() 415 | { 416 | start := time.Now() 417 | res, err := udpAwaitPacket(ctx, nil, []any{conn2, 10}) 418 | require.Less(t, time.Since(start), 20*time.Millisecond) 419 | require.Equal(t, []any{5, "Timeout"}, err) 420 | require.Nil(t, res) 421 | } 422 | { 423 | res, err := udpAwaitPacket(ctx, nil, []any{conn2, 0}) 424 | require.Nil(t, err) 425 | require.Equal(t, 5, res.([]any)[0]) 426 | 427 | res2, err := udpRead(ctx, nil, []any{conn2, 100, 0}) 428 | require.Nil(t, err) 429 | require.Equal(t, []uint8("Three"), res2) 430 | } 431 | 432 | { 433 | res, err := udpClose(ctx, nil, []any{conn1}) 434 | require.Nil(t, err) 435 | require.Equal(t, "", res) 436 | } 437 | { 438 | res, err := udpClose(ctx, nil, []any{conn2}) 439 | require.Nil(t, err) 440 | require.Equal(t, "", res) 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /internal/network-api/network-api.go: -------------------------------------------------------------------------------- 1 | // This file is part of arduino-router 2 | // 3 | // Copyright 2025 ARDUINO SA (http://www.arduino.cc/) 4 | // 5 | // This software is released under the GNU General Public License version 3, 6 | // which covers the main part of arduino-router 7 | // The terms of this license can be found at: 8 | // https://www.gnu.org/licenses/gpl-3.0.en.html 9 | // 10 | // You can be released from the requirements of the above licenses by purchasing 11 | // a commercial license. Buying such a license is mandatory if you want to 12 | // modify or otherwise use the software for commercial activities involving the 13 | // Arduino software without disclosing the source code of your own applications. 14 | // To purchase a commercial license, send an email to license@arduino.cc. 15 | 16 | package networkapi 17 | 18 | import ( 19 | "context" 20 | "crypto/tls" 21 | "crypto/x509" 22 | "errors" 23 | "fmt" 24 | "net" 25 | "os" 26 | "strconv" 27 | "sync" 28 | "sync/atomic" 29 | "time" 30 | 31 | "github.com/arduino/arduino-router/internal/msgpackrouter" 32 | "github.com/arduino/arduino-router/msgpackrpc" 33 | ) 34 | 35 | // Register the Network API methods 36 | func Register(router *msgpackrouter.Router) { 37 | _ = router.RegisterMethod("tcp/connect", tcpConnect) 38 | 39 | _ = router.RegisterMethod("tcp/listen", tcpListen) 40 | _ = router.RegisterMethod("tcp/closeListener", tcpCloseListener) 41 | 42 | _ = router.RegisterMethod("tcp/accept", tcpAccept) 43 | _ = router.RegisterMethod("tcp/read", tcpRead) 44 | _ = router.RegisterMethod("tcp/write", tcpWrite) 45 | _ = router.RegisterMethod("tcp/close", tcpClose) 46 | 47 | _ = router.RegisterMethod("tcp/connectSSL", tcpConnectSSL) 48 | 49 | _ = router.RegisterMethod("udp/connect", udpConnect) 50 | _ = router.RegisterMethod("udp/beginPacket", udpBeginPacket) 51 | _ = router.RegisterMethod("udp/write", udpWrite) 52 | _ = router.RegisterMethod("udp/endPacket", udpEndPacket) 53 | _ = router.RegisterMethod("udp/awaitPacket", udpAwaitPacket) 54 | _ = router.RegisterMethod("udp/read", udpRead) 55 | _ = router.RegisterMethod("udp/dropPacket", udpDropPacket) 56 | _ = router.RegisterMethod("udp/close", udpClose) 57 | } 58 | 59 | var lock sync.RWMutex 60 | var liveConnections = make(map[uint]net.Conn) 61 | var liveListeners = make(map[uint]net.Listener) 62 | var liveUdpConnections = make(map[uint]net.PacketConn) 63 | var udpReadBuffers = make(map[uint][]byte) 64 | var udpWriteTargets = make(map[uint]*net.UDPAddr) 65 | var udpWriteBuffers = make(map[uint][]byte) 66 | var nextConnectionID atomic.Uint32 67 | 68 | // takeLockAndGenerateNextID generates a new unique ID for a connection or listener. 69 | // It locks the global lock to ensure thread safety and checks for existing IDs. 70 | // It returns the new ID and a function to unlock the global lock. 71 | func takeLockAndGenerateNextID() (newID uint, unlock func()) { 72 | lock.Lock() 73 | for { 74 | id := uint(nextConnectionID.Add(1)) 75 | _, exists1 := liveConnections[id] 76 | _, exists2 := liveListeners[id] 77 | if !exists1 && !exists2 { 78 | return id, func() { 79 | lock.Unlock() 80 | } 81 | } 82 | } 83 | } 84 | 85 | func tcpConnect(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 86 | if len(params) != 2 { 87 | return nil, []any{1, "Invalid number of parameters, expected server address and port"} 88 | } 89 | serverAddr, ok := params[0].(string) 90 | if !ok { 91 | return nil, []any{1, "Invalid parameter type, expected string for server address"} 92 | } 93 | serverPort, ok := msgpackrpc.ToUint(params[1]) 94 | if !ok { 95 | return nil, []any{1, "Invalid parameter type, expected uint16 for server port"} 96 | } 97 | 98 | serverAddr = net.JoinHostPort(serverAddr, strconv.FormatUint(uint64(serverPort), 10)) 99 | 100 | conn, err := net.Dial("tcp", serverAddr) 101 | if err != nil { 102 | return nil, []any{2, "Failed to connect to server: " + err.Error()} 103 | } 104 | 105 | // Successfully connected to the server 106 | 107 | id, unlock := takeLockAndGenerateNextID() 108 | liveConnections[id] = conn 109 | unlock() 110 | return id, nil 111 | } 112 | 113 | func tcpListen(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 114 | if len(params) != 2 { 115 | return nil, []any{1, "Invalid number of parameters, expected listen address and port"} 116 | } 117 | listenAddr, ok := params[0].(string) 118 | if !ok { 119 | return nil, []any{1, "Invalid parameter type, expected string for listen address"} 120 | } 121 | listenPort, ok := msgpackrpc.ToUint(params[1]) 122 | if !ok { 123 | return nil, []any{1, "Invalid parameter type, expected uint16 for listen port"} 124 | } 125 | 126 | listenAddr = net.JoinHostPort(listenAddr, strconv.FormatUint(uint64(listenPort), 10)) 127 | 128 | listener, err := net.Listen("tcp", listenAddr) 129 | if err != nil { 130 | return nil, []any{2, "Failed to start listening on address: " + err.Error()} 131 | } 132 | 133 | id, unlock := takeLockAndGenerateNextID() 134 | liveListeners[id] = listener 135 | unlock() 136 | return id, nil 137 | } 138 | 139 | func tcpAccept(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 140 | if len(params) != 1 { 141 | return nil, []any{1, "Invalid number of parameters, expected listener ID"} 142 | } 143 | listenerID, ok := msgpackrpc.ToUint(params[0]) 144 | if !ok { 145 | return nil, []any{1, "Invalid parameter type, expected int for listener ID"} 146 | } 147 | 148 | lock.RLock() 149 | listener, exists := liveListeners[listenerID] 150 | lock.RUnlock() 151 | 152 | if !exists { 153 | return nil, []any{2, fmt.Sprintf("Listener not found for ID: %d", listenerID)} 154 | } 155 | 156 | conn, err := listener.Accept() 157 | if err != nil { 158 | return nil, []any{3, "Failed to accept connection: " + err.Error()} 159 | } 160 | 161 | // Successfully accepted a connection 162 | 163 | connID, unlock := takeLockAndGenerateNextID() 164 | liveConnections[connID] = conn 165 | unlock() 166 | return connID, nil 167 | } 168 | 169 | func tcpClose(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 170 | if len(params) != 1 { 171 | return nil, []any{1, "Invalid number of parameters, expected connection ID"} 172 | } 173 | id, ok := msgpackrpc.ToUint(params[0]) 174 | if !ok { 175 | return nil, []any{1, "Invalid parameter type, expected int for connection ID"} 176 | } 177 | 178 | lock.Lock() 179 | conn, existsConn := liveConnections[id] 180 | if existsConn { 181 | delete(liveConnections, id) 182 | } 183 | lock.Unlock() 184 | 185 | if !existsConn { 186 | return nil, []any{2, fmt.Sprintf("Connection not found for ID: %d", id)} 187 | } 188 | 189 | // Close the connection if it exists 190 | // We do not return an error to the caller if the close operation fails, as it is not critical, 191 | // but we only log the error for debugging purposes. 192 | if err := conn.Close(); err != nil { 193 | return err.Error(), nil 194 | } 195 | return "", nil 196 | } 197 | 198 | func tcpCloseListener(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 199 | if len(params) != 1 { 200 | return nil, []any{1, "Invalid number of parameters, expected listener ID"} 201 | } 202 | id, ok := msgpackrpc.ToUint(params[0]) 203 | if !ok { 204 | return nil, []any{1, "Invalid parameter type, expected int for listener ID"} 205 | } 206 | 207 | lock.Lock() 208 | listener, existsListener := liveListeners[id] 209 | if existsListener { 210 | delete(liveListeners, id) 211 | } 212 | lock.Unlock() 213 | 214 | if !existsListener { 215 | return nil, []any{2, fmt.Sprintf("Listener not found for ID: %d", id)} 216 | } 217 | 218 | // Close the listener if it exists 219 | // We do not return an error to the caller if the close operation fails, as it is not critical, 220 | // but we only log the error for debugging purposes. 221 | if err := listener.Close(); err != nil { 222 | return err.Error(), nil 223 | } 224 | return "", nil 225 | } 226 | 227 | func tcpRead(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 228 | if len(params) != 2 && len(params) != 3 { 229 | return nil, []any{1, "Invalid number of parameters, expected (connection ID, max bytes to read[, optional timeout in ms])"} 230 | } 231 | id, ok := msgpackrpc.ToUint(params[0]) 232 | if !ok { 233 | return nil, []any{1, "Invalid parameter type, expected int for connection ID"} 234 | } 235 | lock.RLock() 236 | conn, ok := liveConnections[id] 237 | lock.RUnlock() 238 | if !ok { 239 | return nil, []any{2, fmt.Sprintf("Connection not found for ID: %d", id)} 240 | } 241 | maxBytes, ok := msgpackrpc.ToUint(params[1]) 242 | if !ok { 243 | return nil, []any{1, "Invalid parameter type, expected int for max bytes to read"} 244 | } 245 | var deadline time.Time // default value == no timeout 246 | if len(params) == 2 { 247 | // It seems that there is no way to set a 0 ms timeout (immediate return) on a TCP connection. 248 | // Setting the read deadline to time.Now() will always returns an empty (zero bytes) 249 | // read, so we set it by default to a very short duration in the future (1 ms). 250 | deadline = time.Now().Add(time.Millisecond) 251 | } else if ms, ok := msgpackrpc.ToInt(params[2]); !ok { 252 | return nil, []any{1, "Invalid parameter type, expected int for timeout in ms"} 253 | } else if ms > 0 { 254 | deadline = time.Now().Add(time.Duration(ms) * time.Millisecond) 255 | } 256 | 257 | buffer := make([]byte, maxBytes) 258 | if err := conn.SetReadDeadline(deadline); err != nil { 259 | return nil, []any{3, "Failed to set read timeout: " + err.Error()} 260 | } 261 | n, err := conn.Read(buffer) 262 | if errors.Is(err, os.ErrDeadlineExceeded) { 263 | // timeout 264 | } else if err != nil { 265 | return nil, []any{3, "Failed to read from connection: " + err.Error()} 266 | } 267 | 268 | return buffer[:n], nil 269 | } 270 | 271 | func tcpWrite(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 272 | if len(params) != 2 { 273 | return nil, []any{1, "Invalid number of parameters, expected (connection ID, data to write)"} 274 | } 275 | id, ok := msgpackrpc.ToUint(params[0]) 276 | if !ok { 277 | return nil, []any{1, "Invalid parameter type, expected int for connection ID"} 278 | } 279 | lock.RLock() 280 | conn, ok := liveConnections[id] 281 | lock.RUnlock() 282 | if !ok { 283 | return nil, []any{2, fmt.Sprintf("Connection not found for ID: %d", id)} 284 | } 285 | data, ok := params[1].([]byte) 286 | if !ok { 287 | if dataStr, ok := params[1].(string); ok { 288 | data = []byte(dataStr) 289 | } else { 290 | // If data is not []byte or string, return an error 291 | return nil, []any{1, "Invalid parameter type, expected []byte or string for data to write"} 292 | } 293 | } 294 | 295 | n, err := conn.Write(data) 296 | if err != nil { 297 | return nil, []any{3, "Failed to write to connection: " + err.Error()} 298 | } 299 | 300 | return n, nil 301 | } 302 | 303 | func tcpConnectSSL(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 304 | n := len(params) 305 | if n < 1 || n > 3 { 306 | return nil, []any{1, "Invalid number of parameters, expected server address, port and optional TLS cert"} 307 | } 308 | serverAddr, ok := params[0].(string) 309 | if !ok { 310 | return nil, []any{1, "Invalid parameter type, expected string for server address"} 311 | } 312 | serverPort, ok := msgpackrpc.ToUint(params[1]) 313 | if !ok { 314 | return nil, []any{1, "Invalid parameter type, expected uint16 for server port"} 315 | } 316 | 317 | serverAddr = net.JoinHostPort(serverAddr, strconv.FormatUint(uint64(serverPort), 10)) 318 | 319 | var tlsConfig *tls.Config 320 | if n == 3 { 321 | cert, ok := params[2].(string) 322 | if !ok { 323 | return nil, []any{1, "Invalid parameter type, expected string for TLS cert"} 324 | } 325 | 326 | if len(cert) > 0 { 327 | // parse TLS cert in pem format 328 | certs := x509.NewCertPool() 329 | if !certs.AppendCertsFromPEM([]byte(cert)) { 330 | return nil, []any{1, "Failed to parse TLS certificate"} 331 | } 332 | tlsConfig = &tls.Config{ 333 | MinVersion: tls.VersionTLS12, 334 | RootCAs: certs, 335 | } 336 | } 337 | } 338 | 339 | conn, err := tls.Dial("tcp", serverAddr, tlsConfig) 340 | if err != nil { 341 | return nil, []any{2, "Failed to connect to server: " + err.Error()} 342 | } 343 | 344 | // Successfully connected to the server 345 | 346 | id, unlock := takeLockAndGenerateNextID() 347 | liveConnections[id] = conn 348 | unlock() 349 | return id, nil 350 | } 351 | 352 | func udpConnect(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 353 | if len(params) != 2 { 354 | return nil, []any{1, "Invalid number of parameters, expected server address and port"} 355 | } 356 | serverAddr, ok := params[0].(string) 357 | if !ok { 358 | return nil, []any{1, "Invalid parameter type, expected string for server address"} 359 | } 360 | serverPort, ok := msgpackrpc.ToUint(params[1]) 361 | if !ok { 362 | return nil, []any{1, "Invalid parameter type, expected uint16 for server port"} 363 | } 364 | 365 | serverAddr = net.JoinHostPort(serverAddr, fmt.Sprintf("%d", serverPort)) 366 | udpAddr, err := net.ResolveUDPAddr("udp", serverAddr) 367 | if err != nil { 368 | return nil, []any{2, "Failed to resolve UDP address: " + err.Error()} 369 | } 370 | udpConn, err := net.ListenUDP("udp", udpAddr) 371 | if err != nil { 372 | return nil, []any{2, "Failed to connect to server: " + err.Error()} 373 | } 374 | 375 | // Successfully opened UDP channel 376 | 377 | id, unlock := takeLockAndGenerateNextID() 378 | liveUdpConnections[id] = udpConn 379 | unlock() 380 | return id, nil 381 | } 382 | 383 | func udpBeginPacket(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 384 | if len(params) != 3 { 385 | return nil, []any{1, "Invalid number of parameters, expected udpConnId, dest address, dest port"} 386 | } 387 | id, ok := msgpackrpc.ToUint(params[0]) 388 | if !ok { 389 | return nil, []any{1, "Invalid parameter type, expected int for UDP connection ID"} 390 | } 391 | targetIP, ok := params[1].(string) 392 | if !ok { 393 | return nil, []any{1, "Invalid parameter type, expected string for server address"} 394 | } 395 | targetPort, ok := msgpackrpc.ToUint(params[2]) 396 | if !ok { 397 | return nil, []any{1, "Invalid parameter type, expected uint16 for server port"} 398 | } 399 | 400 | lock.RLock() 401 | defer lock.RUnlock() 402 | if _, ok := liveUdpConnections[id]; !ok { 403 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 404 | } 405 | targetAddr := net.JoinHostPort(targetIP, fmt.Sprintf("%d", targetPort)) 406 | addr, err := net.ResolveUDPAddr("udp", targetAddr) // TODO: This is inefficient, implement some caching 407 | if err != nil { 408 | return nil, []any{3, "Failed to resolve target address: " + err.Error()} 409 | } 410 | udpWriteTargets[id] = addr 411 | udpWriteBuffers[id] = nil 412 | return true, nil 413 | } 414 | 415 | func udpWrite(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 416 | if len(params) != 2 { 417 | return nil, []any{1, "Invalid number of parameters, expected udpConnId, payload"} 418 | } 419 | id, ok := msgpackrpc.ToUint(params[0]) 420 | if !ok { 421 | return nil, []any{1, "Invalid parameter type, expected int for UDP connection ID"} 422 | } 423 | data, ok := params[1].([]byte) 424 | if !ok { 425 | if dataStr, ok := params[1].(string); ok { 426 | data = []byte(dataStr) 427 | } else { 428 | // If data is not []byte or string, return an error 429 | return nil, []any{1, "Invalid parameter type, expected []byte or string for data to write"} 430 | } 431 | } 432 | 433 | lock.RLock() 434 | udpBuffer, ok := udpWriteBuffers[id] 435 | if ok { 436 | udpWriteBuffers[id] = append(udpBuffer, data...) 437 | } 438 | lock.RUnlock() 439 | if !ok { 440 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 441 | } 442 | return len(data), nil 443 | } 444 | 445 | func udpEndPacket(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 446 | if len(params) != 1 { 447 | return nil, []any{1, "Invalid number of parameters, expected expected udpConnId"} 448 | } 449 | id, buffExists := msgpackrpc.ToUint(params[0]) 450 | if !buffExists { 451 | return nil, []any{1, "Invalid parameter type, expected int for UDP connection ID"} 452 | } 453 | 454 | var udpBuffer []byte 455 | var udpAddr *net.UDPAddr 456 | lock.RLock() 457 | udpConn, connExists := liveUdpConnections[id] 458 | if connExists { 459 | udpBuffer, buffExists = udpWriteBuffers[id] 460 | udpAddr = udpWriteTargets[id] 461 | delete(udpWriteBuffers, id) 462 | delete(udpWriteTargets, id) 463 | } 464 | lock.RUnlock() 465 | if !connExists { 466 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 467 | } 468 | if !buffExists { 469 | return nil, []any{3, fmt.Sprintf("No UDP packet begun for ID: %d", id)} 470 | } 471 | 472 | if n, err := udpConn.WriteTo(udpBuffer, udpAddr); err != nil { 473 | return nil, []any{4, "Failed to write to UDP connection: " + err.Error()} 474 | } else { 475 | return n, nil 476 | } 477 | } 478 | 479 | func udpAwaitPacket(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 480 | if len(params) != 1 && len(params) != 2 { 481 | return nil, []any{1, "Invalid number of parameters, expected (UDP connection ID[, optional timeout in ms])"} 482 | } 483 | id, ok := msgpackrpc.ToUint(params[0]) 484 | if !ok { 485 | return nil, []any{1, "Invalid parameter type, expected uint for UDP connection ID"} 486 | } 487 | var deadline time.Time // default value == no timeout 488 | if len(params) == 2 { 489 | if ms, ok := msgpackrpc.ToInt(params[1]); !ok { 490 | return nil, []any{1, "Invalid parameter type, expected int for timeout in ms"} 491 | } else if ms > 0 { 492 | deadline = time.Now().Add(time.Duration(ms) * time.Millisecond) 493 | } 494 | } 495 | 496 | lock.RLock() 497 | udpConn, ok := liveUdpConnections[id] 498 | lock.RUnlock() 499 | if !ok { 500 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 501 | } 502 | if err := udpConn.SetReadDeadline(deadline); err != nil { 503 | return nil, []any{3, "Failed to set read deadline: " + err.Error()} 504 | } 505 | buffer := make([]byte, 64*1024) // 64 KB buffer 506 | n, addr, err := udpConn.ReadFrom(buffer) 507 | if errors.Is(err, os.ErrDeadlineExceeded) { 508 | // timeout 509 | return nil, []any{5, "Timeout"} 510 | } 511 | if err != nil { 512 | return nil, []any{3, "Failed to read from UDP connection: " + err.Error()} 513 | } 514 | host, portStr, err := net.SplitHostPort(addr.String()) 515 | if err != nil { 516 | // Should never fail, but... 517 | return nil, []any{4, "Failed to parse source address: " + err.Error()} 518 | } 519 | port, err := strconv.Atoi(portStr) 520 | if err != nil { 521 | // Should never fail, but... 522 | return nil, []any{4, "Failed to parse source address: " + err.Error()} 523 | } 524 | 525 | lock.Lock() 526 | udpReadBuffers[id] = buffer[:n] 527 | lock.Unlock() 528 | return []any{n, host, port}, nil 529 | } 530 | 531 | func udpDropPacket(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 532 | if len(params) != 1 && len(params) != 2 { 533 | return nil, []any{1, "Invalid number of parameters, expected (UDP connection ID[, optional timeout in ms])"} 534 | } 535 | id, ok := msgpackrpc.ToUint(params[0]) 536 | if !ok { 537 | return nil, []any{1, "Invalid parameter type, expected uint for UDP connection ID"} 538 | } 539 | 540 | lock.RLock() 541 | delete(udpReadBuffers, id) 542 | lock.RUnlock() 543 | if !ok { 544 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 545 | } 546 | return true, nil 547 | } 548 | 549 | func udpRead(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 550 | if len(params) != 2 && len(params) != 3 { 551 | return nil, []any{1, "Invalid number of parameters, expected (UDP connection ID, max bytes to read)"} 552 | } 553 | id, ok := msgpackrpc.ToUint(params[0]) 554 | if !ok { 555 | return nil, []any{1, "Invalid parameter type, expected uint for UDP connection ID"} 556 | } 557 | maxBytes, ok := msgpackrpc.ToUint(params[1]) 558 | if !ok { 559 | return nil, []any{1, "Invalid parameter type, expected uint for max bytes to read"} 560 | } 561 | 562 | lock.Lock() 563 | buffer, exists := udpReadBuffers[id] 564 | n := uint(len(buffer)) 565 | if exists { 566 | // keep the remainder of the buffer for the next read 567 | if n > maxBytes { 568 | udpReadBuffers[id] = buffer[maxBytes:] 569 | n = maxBytes 570 | } else { 571 | delete(udpReadBuffers, id) 572 | } 573 | } 574 | lock.Unlock() 575 | 576 | return buffer[:n], nil 577 | } 578 | 579 | func udpClose(ctx context.Context, rpc *msgpackrpc.Connection, params []any) (_result any, _err any) { 580 | if len(params) != 1 { 581 | return nil, []any{1, "Invalid number of parameters, expected UDP connection ID"} 582 | } 583 | id, ok := msgpackrpc.ToUint(params[0]) 584 | if !ok { 585 | return nil, []any{1, "Invalid parameter type, expected int for UDP connection ID"} 586 | } 587 | 588 | lock.Lock() 589 | udpConn, existsConn := liveUdpConnections[id] 590 | delete(liveUdpConnections, id) 591 | delete(udpReadBuffers, id) 592 | lock.Unlock() 593 | 594 | if !existsConn { 595 | return nil, []any{2, fmt.Sprintf("UDP connection not found for ID: %d", id)} 596 | } 597 | 598 | // Close the connection if it exists 599 | // We do not return an error to the caller if the close operation fails, as it is not critical, 600 | // but we only log the error for debugging purposes. 601 | if err := udpConn.Close(); err != nil { 602 | return err.Error(), nil 603 | } 604 | return "", nil 605 | } 606 | --------------------------------------------------------------------------------